feature: (room timeline): Changed timeline view to a scrollable list / Made single messages/events selectable / Implemented Message sending

This commit is contained in:
antifallobst 2023-07-08 23:09:53 +02:00
parent 658f05b8d3
commit 98f7e806de
4 changed files with 139 additions and 30 deletions

View File

@ -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 => (),
};
},
_ => () _ => ()
}; };
}, },

View File

@ -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");

View File

@ -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
} }

View File

@ -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(())