Add message inspection popup
This commit is contained in:
parent
d92c7cb98e
commit
d7e19b5eca
5 changed files with 196 additions and 8 deletions
|
|
@ -16,6 +16,7 @@ Procedure when bumping the version number:
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Room deletion confirmation popup
|
- Room deletion confirmation popup
|
||||||
|
- Message inspection popup
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Cursor being visible through popups
|
- Cursor being visible through popups
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod account;
|
mod account;
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod inspect;
|
||||||
mod links;
|
mod links;
|
||||||
mod nick;
|
mod nick;
|
||||||
mod nick_list;
|
mod nick_list;
|
||||||
|
|
|
||||||
94
src/ui/euph/inspect.rs
Normal file
94
src/ui/euph/inspect.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
|
use euphoxide::api::Message;
|
||||||
|
use toss::styled::Styled;
|
||||||
|
|
||||||
|
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
||||||
|
use crate::ui::widgets::popup::Popup;
|
||||||
|
use crate::ui::widgets::text::Text;
|
||||||
|
use crate::ui::widgets::BoxedWidget;
|
||||||
|
|
||||||
|
macro_rules! line {
|
||||||
|
( $text:ident, $name:expr, $val:expr ) => {
|
||||||
|
$text = $text
|
||||||
|
.then($name, ContentStyle::default().cyan())
|
||||||
|
.then_plain(format!(" {}\n", $val));
|
||||||
|
};
|
||||||
|
( $text:ident, $name:expr, $val:expr, debug ) => {
|
||||||
|
$text = $text
|
||||||
|
.then($name, ContentStyle::default().cyan())
|
||||||
|
.then_plain(format!(" {:?}\n", $val));
|
||||||
|
};
|
||||||
|
( $text:ident, $name:expr, $val:expr, optional ) => {
|
||||||
|
if let Some(val) = $val {
|
||||||
|
$text = $text
|
||||||
|
.then($name, ContentStyle::default().cyan())
|
||||||
|
.then_plain(format!(" {val}\n"));
|
||||||
|
} else {
|
||||||
|
$text = $text
|
||||||
|
.then($name, ContentStyle::default().cyan())
|
||||||
|
.then_plain(" ")
|
||||||
|
.then("none", ContentStyle::default().italic().grey())
|
||||||
|
.then_plain("\n");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
( $text:ident, $name:expr, $val:expr, yes or no ) => {
|
||||||
|
$text = $text
|
||||||
|
.then($name, ContentStyle::default().cyan())
|
||||||
|
.then_plain(if $val { " yes\n" } else { " no\n" });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message_widget(msg: &Message) -> BoxedWidget {
|
||||||
|
let heading_style = ContentStyle::default().bold();
|
||||||
|
|
||||||
|
let mut text = Styled::new("Message", heading_style).then_plain("\n");
|
||||||
|
line!(text, "id", msg.id);
|
||||||
|
line!(text, "parent", msg.parent, optional);
|
||||||
|
line!(text, "previous_edit_id", msg.previous_edit_id, optional);
|
||||||
|
line!(text, "time", msg.time.0);
|
||||||
|
line!(text, "encryption_key_id", &msg.encryption_key_id, optional);
|
||||||
|
line!(text, "edited", msg.edited.map(|t| t.0), optional);
|
||||||
|
line!(text, "deleted", msg.deleted.map(|t| t.0), optional);
|
||||||
|
line!(text, "truncated", msg.truncated, yes or no);
|
||||||
|
text = text.then_plain("\n");
|
||||||
|
|
||||||
|
text = text.then("Sender", heading_style).then_plain("\n");
|
||||||
|
line!(text, "id", msg.sender.id);
|
||||||
|
line!(text, "name", msg.sender.name);
|
||||||
|
line!(text, "name (raw)", msg.sender.name, debug);
|
||||||
|
line!(text, "server_id", msg.sender.server_id);
|
||||||
|
line!(text, "server_era", msg.sender.server_era);
|
||||||
|
line!(text, "session_id", msg.sender.session_id);
|
||||||
|
line!(text, "is_staff", msg.sender.is_staff, yes or no);
|
||||||
|
line!(text, "is_manager", msg.sender.is_manager, yes or no);
|
||||||
|
line!(
|
||||||
|
text,
|
||||||
|
"client_address",
|
||||||
|
msg.sender.client_address.as_ref(),
|
||||||
|
optional
|
||||||
|
);
|
||||||
|
line!(
|
||||||
|
text,
|
||||||
|
"real_client_address",
|
||||||
|
msg.sender.real_client_address.as_ref(),
|
||||||
|
optional
|
||||||
|
);
|
||||||
|
|
||||||
|
Popup::new(Text::new(text)).title("Inspect message").build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_key_bindings(bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.binding("esc", "close");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum EventResult {
|
||||||
|
NotHandled,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input_event(event: &InputEvent) -> EventResult {
|
||||||
|
match event {
|
||||||
|
key!(Esc) => EventResult::Close,
|
||||||
|
_ => EventResult::NotHandled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::style::{ContentStyle, Stylize};
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
use euphoxide::api::{Data, PacketType, Snowflake};
|
use euphoxide::api::{Data, Message, PacketType, Snowflake};
|
||||||
use euphoxide::conn::{Joined, Joining, Status};
|
use euphoxide::conn::{Joined, Joining, Status};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
|
|
@ -30,15 +30,17 @@ use crate::vault::EuphVault;
|
||||||
use super::account::{self, AccountUiState};
|
use super::account::{self, AccountUiState};
|
||||||
use super::links::{self, LinksState};
|
use super::links::{self, LinksState};
|
||||||
use super::popup::RoomPopup;
|
use super::popup::RoomPopup;
|
||||||
use super::{auth, nick, nick_list};
|
use super::{auth, inspect, nick, nick_list};
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
enum State {
|
enum State {
|
||||||
Normal,
|
Normal,
|
||||||
Auth(EditorState),
|
Auth(EditorState),
|
||||||
Nick(EditorState),
|
Nick(EditorState),
|
||||||
Account(AccountUiState),
|
Account(AccountUiState),
|
||||||
Links(LinksState),
|
Links(LinksState),
|
||||||
// TODO Inspect messages and users
|
InspectMessage(Message),
|
||||||
|
// TODO Inspect users
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
|
@ -225,6 +227,7 @@ impl EuphRoom {
|
||||||
State::Nick(editor) => layers.push(nick::widget(editor)),
|
State::Nick(editor) => layers.push(nick::widget(editor)),
|
||||||
State::Account(account) => layers.push(account.widget()),
|
State::Account(account) => layers.push(account.widget()),
|
||||||
State::Links(links) => layers.push(links.widget()),
|
State::Links(links) => layers.push(links.widget()),
|
||||||
|
State::InspectMessage(message) => layers.push(inspect::message_widget(message)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for popup in &self.popups {
|
for popup in &self.popups {
|
||||||
|
|
@ -319,6 +322,7 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bindings.binding("i", "inspect message");
|
||||||
bindings.binding("I", "show message links");
|
bindings.binding("I", "show message links");
|
||||||
|
|
||||||
bindings.empty();
|
bindings.empty();
|
||||||
|
|
@ -353,7 +357,16 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let key!('I') = event {
|
match event {
|
||||||
|
key!('i') => {
|
||||||
|
if let Some(id) = self.chat.cursor().await {
|
||||||
|
if let Some(msg) = self.vault.full_msg(id).await {
|
||||||
|
self.state = State::InspectMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
key!('I') => {
|
||||||
if let Some(id) = self.chat.cursor().await {
|
if let Some(id) = self.chat.cursor().await {
|
||||||
if let Some(msg) = self.vault.msg(&id).await {
|
if let Some(msg) = self.vault.msg(&id).await {
|
||||||
self.state = State::Links(LinksState::new(&msg.content));
|
self.state = State::Links(LinksState::new(&msg.content));
|
||||||
|
|
@ -361,6 +374,8 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
match status.ok().flatten() {
|
match status.ok().flatten() {
|
||||||
Some(Status::Joining(Joining {
|
Some(Status::Joining(Joining {
|
||||||
|
|
@ -425,6 +440,7 @@ impl EuphRoom {
|
||||||
State::Nick(_) => nick::list_key_bindings(bindings),
|
State::Nick(_) => nick::list_key_bindings(bindings),
|
||||||
State::Account(account) => account.list_key_bindings(bindings),
|
State::Account(account) => account.list_key_bindings(bindings),
|
||||||
State::Links(links) => links.list_key_bindings(bindings),
|
State::Links(links) => links.list_key_bindings(bindings),
|
||||||
|
State::InspectMessage(_) => inspect::list_key_bindings(bindings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,6 +458,8 @@ impl EuphRoom {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Use a common EventResult
|
||||||
|
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
State::Normal => {
|
State::Normal => {
|
||||||
self.handle_normal_input_event(terminal, crossterm_lock, event)
|
self.handle_normal_input_event(terminal, crossterm_lock, event)
|
||||||
|
|
@ -494,6 +512,13 @@ impl EuphRoom {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
State::InspectMessage(_) => match inspect::handle_input_event(event) {
|
||||||
|
inspect::EventResult::NotHandled => false,
|
||||||
|
inspect::EventResult::Close => {
|
||||||
|
self.state = State::Normal;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// TODO Reduce code duplication (macro?)
|
||||||
|
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -146,6 +148,18 @@ impl EuphVault {
|
||||||
rx.await.unwrap()
|
rx.await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn full_msg(&self, id: Snowflake) -> Option<Message> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::GetFullMsg {
|
||||||
|
room: self.room.clone(),
|
||||||
|
id,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.vault.tx.send(request.into());
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn chunk_at_offset(&self, amount: usize, offset: usize) -> Vec<Message> {
|
pub async fn chunk_at_offset(&self, amount: usize, offset: usize) -> Vec<Message> {
|
||||||
// TODO vault::Error
|
// TODO vault::Error
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
@ -420,6 +434,11 @@ pub(super) enum EuphRequest {
|
||||||
id: Snowflake,
|
id: Snowflake,
|
||||||
result: oneshot::Sender<Option<SmallMessage>>,
|
result: oneshot::Sender<Option<SmallMessage>>,
|
||||||
},
|
},
|
||||||
|
GetFullMsg {
|
||||||
|
room: String,
|
||||||
|
id: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Message>>,
|
||||||
|
},
|
||||||
GetTree {
|
GetTree {
|
||||||
room: String,
|
room: String,
|
||||||
root: Snowflake,
|
root: Snowflake,
|
||||||
|
|
@ -524,6 +543,7 @@ impl EuphRequest {
|
||||||
Self::GetLastSpan { room, result } => Self::get_last_span(conn, room, result),
|
Self::GetLastSpan { room, result } => Self::get_last_span(conn, room, result),
|
||||||
Self::GetPath { room, id, result } => Self::get_path(conn, room, id, result),
|
Self::GetPath { room, id, result } => Self::get_path(conn, room, id, result),
|
||||||
Self::GetMsg { room, id, result } => Self::get_msg(conn, room, id, result),
|
Self::GetMsg { room, id, result } => Self::get_msg(conn, room, id, result),
|
||||||
|
Self::GetFullMsg { room, id, result } => Self::get_full_msg(conn, room, id, result),
|
||||||
Self::GetTree { room, root, result } => Self::get_tree(conn, room, root, result),
|
Self::GetTree { room, root, result } => Self::get_tree(conn, room, root, result),
|
||||||
Self::GetFirstTreeId { room, result } => Self::get_first_tree_id(conn, room, result),
|
Self::GetFirstTreeId { room, result } => Self::get_first_tree_id(conn, room, result),
|
||||||
Self::GetLastTreeId { room, result } => Self::get_last_tree_id(conn, room, result),
|
Self::GetLastTreeId { room, result } => Self::get_last_tree_id(conn, room, result),
|
||||||
|
|
@ -949,6 +969,53 @@ impl EuphRequest {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_full_msg(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
id: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Message>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let mut query = conn.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
||||||
|
FROM euph_msgs
|
||||||
|
WHERE room = ?
|
||||||
|
AND id = ?
|
||||||
|
"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let msg = query
|
||||||
|
.query_row(params![room, WSnowflake(id)], |row| {
|
||||||
|
Ok(Message {
|
||||||
|
id: row.get::<_, WSnowflake>(0)?.0,
|
||||||
|
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| s.0),
|
||||||
|
previous_edit_id: row.get::<_, Option<WSnowflake>>(2)?.map(|s| s.0),
|
||||||
|
time: row.get::<_, WTime>(3)?.0,
|
||||||
|
content: row.get(4)?,
|
||||||
|
encryption_key_id: row.get(5)?,
|
||||||
|
edited: row.get::<_, Option<WTime>>(6)?.map(|t| t.0),
|
||||||
|
deleted: row.get::<_, Option<WTime>>(7)?.map(|t| t.0),
|
||||||
|
truncated: row.get(8)?,
|
||||||
|
sender: SessionView {
|
||||||
|
id: UserId(row.get(9)?),
|
||||||
|
name: row.get(10)?,
|
||||||
|
server_id: row.get(11)?,
|
||||||
|
server_era: row.get(12)?,
|
||||||
|
session_id: row.get(13)?,
|
||||||
|
is_staff: row.get(14)?,
|
||||||
|
is_manager: row.get(15)?,
|
||||||
|
client_address: row.get(16)?,
|
||||||
|
real_client_address: row.get(17)?,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.optional()?;
|
||||||
|
let _ = result.send(msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_tree(
|
fn get_tree(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
room: String,
|
room: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue