feature: (room timeline): Changed timeline view to a scrollable list / Made single messages/events selectable / Implemented Message sending
This commit is contained in:
parent
658f05b8d3
commit
98f7e806de
|
@ -108,20 +108,84 @@ impl Event {
|
||||||
}
|
}
|
||||||
input => {
|
input => {
|
||||||
match app.ui.input_position() {
|
match app.ui.input_position() {
|
||||||
ui::MainInputPosition::MessageCompose => { app.ui.message_compose.input(input); },
|
ui::MainInputPosition::MessageCompose => {
|
||||||
|
match input {
|
||||||
|
tui_textarea::Input {key: tui_textarea::Key::Enter, alt: true, ..} => {
|
||||||
|
match app.status.room_mut() {
|
||||||
|
Some(room) => {
|
||||||
|
room.send(app.ui.message_compose.lines().join("\n")).await?;
|
||||||
|
app.ui.message_compose_clear();
|
||||||
|
},
|
||||||
|
None => ()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => { app.ui.message_compose.input(input); }
|
||||||
|
};
|
||||||
|
},
|
||||||
ui::MainInputPosition::Rooms => {
|
ui::MainInputPosition::Rooms => {
|
||||||
match input {
|
match input {
|
||||||
tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
|
tui_textarea::Input {key: tui_textarea::Key::Up, ..} => {
|
||||||
let i = match app.ui.rooms_state.selected() {
|
let i = match app.ui.rooms_state.selected() {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
if i >= app.status.rooms().len() - 1 { 0 }
|
if i > 0 { i - 1 }
|
||||||
else { i + 1 }
|
else { i }
|
||||||
},
|
},
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
app.ui.rooms_state.select(Some(i));
|
app.ui.rooms_state.select(Some(i));
|
||||||
app.status.set_room_by_index(i)?;
|
app.status.set_room_by_index(i)?;
|
||||||
},
|
},
|
||||||
|
tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
|
||||||
|
let i = match app.ui.rooms_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i < app.status.rooms().len() { i + 1 }
|
||||||
|
else { i }
|
||||||
|
},
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
app.ui.rooms_state.select(Some(i));
|
||||||
|
app.status.set_room_by_index(i)?;
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ui::MainInputPosition::Messages => {
|
||||||
|
match input {
|
||||||
|
tui_textarea::Input {key: tui_textarea::Key::Up, ..} => {
|
||||||
|
match app.status.room_mut(){
|
||||||
|
Some(room) => {
|
||||||
|
let len = room.timeline().len();
|
||||||
|
let i = match room.view_scroll() {
|
||||||
|
Some(i) => i+1,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
if i < len {
|
||||||
|
room.set_view_scroll(Some(i))
|
||||||
|
}
|
||||||
|
if i <= len - 5 {
|
||||||
|
room.poll_old_timeline().await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
tui_textarea::Input {key: tui_textarea::Key::Down, ..} => {
|
||||||
|
match app.status.room_mut(){
|
||||||
|
Some(room) => {
|
||||||
|
match room.view_scroll() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
room.set_view_scroll(None);
|
||||||
|
} else {
|
||||||
|
room.set_view_scroll(Some(i - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
},
|
||||||
_ => ()
|
_ => ()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -142,9 +142,7 @@ impl App<'_> {
|
||||||
|
|
||||||
for (_, room) in self.status.rooms_mut() {
|
for (_, room) in self.status.rooms_mut() {
|
||||||
room.update_name().await?;
|
room.update_name().await?;
|
||||||
room.poll_old_timeline().await?;
|
for _ in 0..3 { room.poll_old_timeline().await?; }
|
||||||
room.poll_old_timeline().await?;
|
|
||||||
room.poll_old_timeline().await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Initializing client for the current account");
|
info!("Initializing client for the current account");
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
use std::any::Any;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use matrix_sdk::{Client,
|
use matrix_sdk::{Client,
|
||||||
ruma::{events::{AnyTimelineEvent}, RoomId},
|
ruma::{events::{AnyTimelineEvent,
|
||||||
|
room::message::RoomMessageEventContent,
|
||||||
|
StateEventType},
|
||||||
|
RoomId,
|
||||||
|
TransactionId},
|
||||||
room::MessagesOptions};
|
room::MessagesOptions};
|
||||||
use anyhow::{Result, Error};
|
use anyhow::{Result, Error};
|
||||||
use cli_log::{error, warn, info};
|
use cli_log::{error, warn, info};
|
||||||
|
@ -16,6 +21,7 @@ pub struct Room {
|
||||||
name: String,
|
name: String,
|
||||||
timeline: Vec<AnyTimelineEvent>,
|
timeline: Vec<AnyTimelineEvent>,
|
||||||
timeline_end: Option<String>,
|
timeline_end: Option<String>,
|
||||||
|
view_scroll: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
|
@ -35,10 +41,17 @@ impl Room {
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
timeline: Vec::new(),
|
timeline: Vec::new(),
|
||||||
timeline_end: None,
|
timeline_end: None,
|
||||||
|
view_scroll: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn poll_old_timeline(&mut self) -> Result<()> {
|
pub async fn poll_old_timeline(&mut self) -> Result<()> {
|
||||||
|
if let Some(AnyTimelineEvent::State(event)) = &self.timeline.get(0) {
|
||||||
|
if event.event_type() == StateEventType::RoomCreate {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut messages_options = MessagesOptions::backward();
|
let mut messages_options = MessagesOptions::backward();
|
||||||
messages_options = match &self.timeline_end {
|
messages_options = match &self.timeline_end {
|
||||||
Some(end) => messages_options.from(end.as_str()),
|
Some(end) => messages_options.from(end.as_str()),
|
||||||
|
@ -73,6 +86,21 @@ impl Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timeline(&self) -> &Vec<AnyTimelineEvent> { &self.timeline }
|
pub fn timeline(&self) -> &Vec<AnyTimelineEvent> { &self.timeline }
|
||||||
|
|
||||||
|
pub async fn send(&mut self, message: String) -> Result<()> {
|
||||||
|
let content = RoomMessageEventContent::text_plain(message);
|
||||||
|
let id = TransactionId::new();
|
||||||
|
self.matrix_room.send(content, Some(&id)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_scroll(&self) -> Option<usize> {
|
||||||
|
self.view_scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_view_scroll(&mut self, scroll: Option<usize>) {
|
||||||
|
self.view_scroll = scroll;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
|
@ -116,6 +144,10 @@ impl Status {
|
||||||
self.rooms.get(self.current_room_id.as_str())
|
self.rooms.get(self.current_room_id.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn room_mut(&mut self) -> Option<&mut Room> {
|
||||||
|
self.rooms.get_mut(self.current_room_id.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rooms(&self) -> &IndexMap<String, Room> {
|
pub fn rooms(&self) -> &IndexMap<String, Room> {
|
||||||
&self.rooms
|
&self.rooms
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use anyhow::{Error, Result};
|
||||||
use std::io::Stdout;
|
use std::io::Stdout;
|
||||||
use std::io;
|
use std::io;
|
||||||
use tui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders, Widget}, Terminal, Frame};
|
use tui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders, Widget}, Terminal, Frame};
|
||||||
use tui::layout::Alignment;
|
use tui::layout::{Alignment, Corner};
|
||||||
use tui::style::{Color, Modifier, Style};
|
use tui::style::{Color, Modifier, Style};
|
||||||
use tui::text::{Spans, Span, Text};
|
use tui::text::{Spans, Span, Text};
|
||||||
use tui::widgets::{List, ListItem, ListState, Paragraph, Wrap};
|
use tui::widgets::{List, ListItem, ListState, Paragraph, Wrap};
|
||||||
|
@ -225,7 +225,7 @@ impl UI<'_> {
|
||||||
let mut message_compose = TextArea::default();
|
let mut message_compose = TextArea::default();
|
||||||
message_compose.set_block(
|
message_compose.set_block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title("Message Compose")
|
.title("Message Compose (send: <Alt>+<Enter>)")
|
||||||
.borders(Borders::ALL));
|
.borders(Borders::ALL));
|
||||||
|
|
||||||
info!("Initialized UI");
|
info!("Initialized UI");
|
||||||
|
@ -251,6 +251,14 @@ impl UI<'_> {
|
||||||
|
|
||||||
pub fn input_position(&self) -> &MainInputPosition { &self.input_position }
|
pub fn input_position(&self) -> &MainInputPosition { &self.input_position }
|
||||||
|
|
||||||
|
pub fn message_compose_clear(&mut self) {
|
||||||
|
self.message_compose = TextArea::default();
|
||||||
|
self.message_compose.set_block(
|
||||||
|
Block::default()
|
||||||
|
.title("Message Compose (send: <Alt>+<Enter>)")
|
||||||
|
.borders(Borders::ALL));
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self, status: &Status) -> Result<()> {
|
pub async fn update(&mut self, status: &Status) -> Result<()> {
|
||||||
let chunk = self.terminal.size()?;
|
let chunk = self.terminal.size()?;
|
||||||
|
|
||||||
|
@ -289,6 +297,7 @@ impl UI<'_> {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
r.timeline()
|
r.timeline()
|
||||||
.iter()
|
.iter()
|
||||||
|
.rev()
|
||||||
.map(|event| {
|
.map(|event| {
|
||||||
match event {
|
match event {
|
||||||
|
|
||||||
|
@ -306,28 +315,33 @@ impl UI<'_> {
|
||||||
},
|
},
|
||||||
_ => ("~~~ not supported message like event ~~~".to_string(), Color::Red)
|
_ => ("~~~ not supported message like event ~~~".to_string(), Color::Red)
|
||||||
};
|
};
|
||||||
Spans::from(vec![
|
let mut text = Text::styled(message_like_event.sender().to_string(),
|
||||||
Span::styled(message_like_event.sender().to_string(), Style::default().fg(Color::Cyan)),
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
|
||||||
Span::styled(": ", Style::default().fg(Color::Cyan)),
|
text.extend(Text::styled(content.to_string(),
|
||||||
Span::styled(content, Style::default().fg(color)),
|
Style::default().fg(color)));
|
||||||
])
|
ListItem::new(text)
|
||||||
},
|
},
|
||||||
|
|
||||||
// State Events
|
// State Events
|
||||||
AnyTimelineEvent::State(state) => {
|
AnyTimelineEvent::State(state) => {
|
||||||
Spans::from(vec![
|
ListItem::new(vec![Spans::from(vec![
|
||||||
Span::styled(state.sender().to_string(), Style::default().fg(Color::DarkGray)),
|
Span::styled(state.sender().to_string(), Style::default().fg(Color::DarkGray)),
|
||||||
Span::styled(": ", Style::default().fg(Color::DarkGray)),
|
Span::styled(": ", Style::default().fg(Color::DarkGray)),
|
||||||
Span::styled(state.event_type().to_string(), Style::default().fg(Color::DarkGray))
|
Span::styled(state.event_type().to_string(), Style::default().fg(Color::DarkGray))
|
||||||
])
|
])])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
vec![Spans::from(Span::styled("No room selected!", Style::default().fg(Color::Magenta)))]
|
vec![ListItem::new(Text::styled("No room selected!", Style::default().fg(Color::Magenta)))]
|
||||||
},
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut messages_state = ListState::default();
|
||||||
|
if let Some(room) = status.room() {
|
||||||
|
messages_state.select(room.view_scroll());
|
||||||
};
|
};
|
||||||
|
|
||||||
// calculate to widgets colors, based of which widget is currently selected
|
// calculate to widgets colors, based of which widget is currently selected
|
||||||
|
@ -355,31 +369,32 @@ impl UI<'_> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// initiate the widgets
|
// initiate the widgets
|
||||||
let status = Paragraph::new(status_content)
|
let status_panel = Paragraph::new(status_content)
|
||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.title("Status")
|
.title("Status")
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Status as usize])))
|
.style(Style::default().fg(colors[MainInputPosition::Status as usize])))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
let rooms = List::new(rooms_content)
|
let rooms_panel = List::new(rooms_content)
|
||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.title("Rooms")
|
.title("Rooms (navigate: arrow keys)")
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])))
|
.style(Style::default().fg(colors[MainInputPosition::Rooms as usize])))
|
||||||
.style(Style::default().fg(Color::DarkGray))
|
.style(Style::default().fg(Color::DarkGray))
|
||||||
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||||
.highlight_symbol(">");
|
.highlight_symbol(">");
|
||||||
|
|
||||||
let messages = Paragraph::new(messages_content)
|
let messages_panel = List::new(messages_content)
|
||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.title("Messages")
|
.title("Messages")
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])))
|
.style(Style::default().fg(colors[MainInputPosition::Messages as usize])))
|
||||||
.alignment(Alignment::Left)
|
.start_corner(Corner::BottomLeft)
|
||||||
.wrap(Wrap {trim: false});
|
.highlight_symbol(">")
|
||||||
|
.highlight_style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
let room_info = Block::default()
|
let room_info_panel = Block::default()
|
||||||
.title("Room Info")
|
.title("Room Info")
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize]));
|
.style(Style::default().fg(colors[MainInputPosition::RoomInfo as usize]));
|
||||||
|
@ -387,11 +402,11 @@ impl UI<'_> {
|
||||||
|
|
||||||
// render the widgets
|
// render the widgets
|
||||||
self.terminal.draw(|frame| {
|
self.terminal.draw(|frame| {
|
||||||
frame.render_widget(status, left_chunks[0]);
|
frame.render_widget(status_panel, left_chunks[0]);
|
||||||
frame.render_stateful_widget(rooms, left_chunks[1], &mut self.rooms_state);
|
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
|
||||||
frame.render_widget(messages, middle_chunks[0]);
|
frame.render_stateful_widget(messages_panel, middle_chunks[0], & mut messages_state);
|
||||||
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
|
frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
|
||||||
frame.render_widget(room_info, right_chunks[0]);
|
frame.render_widget(room_info_panel, right_chunks[0]);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Reference in New Issue