From a129f9635fb41f5beadaac95c3c9c0e899423c22 Mon Sep 17 00:00:00 2001 From: Phil Kulak Date: Mon, 10 Apr 2023 21:36:27 -0700 Subject: [PATCH] Open files. --- Cargo.lock | 11 ++++ Cargo.toml | 2 + src/app.rs | 2 +- src/matrix/matrix.rs | 137 ++++++++++++++++++++++++++----------------- src/spawn.rs | 14 +++++ src/widgets/chat.rs | 50 ++++++++++++++-- 6 files changed, 155 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 764ba24..7505ab4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,7 +1750,9 @@ dependencies = [ "futures", "log", "matrix-sdk", + "mime", "once_cell", + "open", "rand 0.8.5", "ruma", "serde", @@ -1909,6 +1911,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075c5203b3a2b698bc72c6c10b1f6263182135751d5013ea66e8a4b3d0562a43" +dependencies = [ + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.45" diff --git a/Cargo.toml b/Cargo.toml index b7d640c..8f8c8bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ dirs = "5.0" emojis = "0.5" futures = "0.3.24" log = "0.4" +mime = "0.3" once_cell = "1.17" +open = "4.0" rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } simple-logging = "2.0" diff --git a/src/app.rs b/src/app.rs index e660fab..ebd7bdf 100644 --- a/src/app.rs +++ b/src/app.rs @@ -44,7 +44,7 @@ pub struct App { impl App { pub fn new(send: Sender) -> Self { - let matrix = Matrix::new(send.clone()); + let matrix = Matrix::default(); // Save the sender for future threads. SENDER diff --git a/src/matrix/matrix.rs b/src/matrix/matrix.rs index b1bbbff..ff607c6 100644 --- a/src/matrix/matrix.rs +++ b/src/matrix/matrix.rs @@ -5,17 +5,19 @@ use std::sync::Arc; use crate::app::App; use crate::event::Event; -use crate::event::Event::{Matui, Tick}; +use crate::event::Event::Matui; use crate::handler::MatuiEvent::{ Error, ProgressComplete, ProgressStarted, VerificationCompleted, VerificationStarted, }; use crate::handler::{Batch, MatuiEvent, SyncType}; use crate::matrix::roomcache::{DecoratedRoom, RoomCache}; +use crate::spawn::view_file; use anyhow::{bail, Context}; use futures::stream::StreamExt; use log::{error, info, warn}; use matrix_sdk::config::SyncSettings; use matrix_sdk::encryption::verification::{Emoji, SasState, SasVerification, Verification}; +use matrix_sdk::media::{MediaFormat, MediaRequest}; use matrix_sdk::room::{Joined, MessagesOptions, Room}; use matrix_sdk::ruma::api::client::filter::{ FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter, @@ -34,6 +36,8 @@ use rand::rngs::OsRng; use rand::{distributions::Alphanumeric, Rng}; use ruma::events::reaction::ReactionEventContent; use ruma::events::relation::Annotation; +use ruma::events::room::message::MessageType::Image; +use ruma::events::room::message::MessageType::Video; use ruma::events::room::message::RoomMessageEventContent; use ruma::events::{AnySyncTimelineEvent, AnyTimelineEvent}; use ruma::{OwnedEventId, UInt}; @@ -46,11 +50,10 @@ pub struct Matrix { rt: Arc, client: Arc>, room_cache: Arc, - send: Sender, } -impl Matrix { - pub fn new(send: Sender) -> Matrix { +impl Default for Matrix { + fn default() -> Self { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() @@ -61,10 +64,11 @@ impl Matrix { rt: Arc::new(rt), client: Arc::new(OnceCell::default()), room_cache: Arc::new(RoomCache::default()), - send, } } +} +impl Matrix { fn dirs() -> (PathBuf, PathBuf) { let data_dir = dirs::data_dir() .expect("no data directory found") @@ -81,8 +85,8 @@ impl Matrix { .to_owned() } - fn send(&self, event: MatuiEvent) { - self.send + fn send(event: MatuiEvent) { + App::get_sender() .send(Matui(event)) .expect("could not send Matrix event"); } @@ -93,19 +97,19 @@ impl Matrix { let (_, session_file) = Matrix::dirs(); if !session_file.exists() { - self.send(MatuiEvent::LoginRequired); + Matrix::send(MatuiEvent::LoginRequired); return; } let matrix = self.clone(); self.rt.spawn(async move { - matrix.send(MatuiEvent::SyncStarted(SyncType::Latest)); + Matrix::send(MatuiEvent::SyncStarted(SyncType::Latest)); let (client, token) = match restore_session(session_file.as_path()).await { Ok(tuple) => tuple, Err(err) => { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); return; } }; @@ -118,13 +122,13 @@ impl Matrix { .expect("could not set client"); if let Err(err) = sync_once(client.clone(), token, &session_file).await { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); return; }; matrix.room_cache.populate(client).await; - matrix.send(MatuiEvent::SyncComplete); + Matrix::send(MatuiEvent::SyncComplete); }); } @@ -135,12 +139,12 @@ impl Matrix { let matrix = self.clone(); self.rt.spawn(async move { - matrix.send(MatuiEvent::LoginStarted); + Matrix::send(MatuiEvent::LoginStarted); let client = match login(&data_dir, &session_file, &user, &pass).await { Ok(client) => client, Err(err) => { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); return; } }; @@ -150,17 +154,17 @@ impl Matrix { .set(client.clone()) .expect("could not set client"); - matrix.send(MatuiEvent::LoginComplete); - matrix.send(MatuiEvent::SyncStarted(SyncType::Initial)); + Matrix::send(MatuiEvent::LoginComplete); + Matrix::send(MatuiEvent::SyncStarted(SyncType::Initial)); if let Err(err) = sync_once(client.clone(), None, &session_file).await { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); return; }; matrix.room_cache.populate(client).await; - matrix.send(MatuiEvent::SyncComplete); + Matrix::send(MatuiEvent::SyncComplete); }); } @@ -199,12 +203,10 @@ impl Matrix { } pub fn confirm_verification(&self, sas: SasVerification) { - let matrix = self.clone(); - self.rt.spawn(async move { if let Err(err) = sas.confirm().await { error!("could not verify: {}", err.to_string()); - matrix.send(Error(format!("Could not verify: {}", err.to_string()))); + Matrix::send(Error(format!("Could not verify: {}", err.to_string()))); } }); } @@ -224,9 +226,6 @@ impl Matrix { } pub fn fetch_messages(&self, room: Joined, cursor: Option) { - let matrix = self.clone(); - let sender = self.send.clone(); - self.rt.spawn(async move { // fetch the actual messages let mut options = MessagesOptions::new(Direction::Backward); @@ -236,7 +235,7 @@ impl Matrix { let messages = match room.messages(options).await { Ok(msg) => msg, Err(err) => { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); return; } }; @@ -253,61 +252,93 @@ impl Matrix { cursor: messages.end, }; - sender - .send(Matui(MatuiEvent::TimelineBatch(batch))) - .expect("could not send messages event"); + Matrix::send(MatuiEvent::TimelineBatch(batch)); // and look up the detail about every user - async fn send_member_event( - msg: &AnyTimelineEvent, - room: Joined, - sender: Sender, - ) -> anyhow::Result<()> { + // this needs to go away + async fn send_member_event(msg: &AnyTimelineEvent, room: Joined) -> anyhow::Result<()> { let member = room .get_member(msg.sender()) .await? .context("not a member")?; - sender.send(Matui(MatuiEvent::Member(member)))?; + Matrix::send(MatuiEvent::Member(member)); Ok(()) } for msg in unpacked { - let sender = sender.clone(); - - if let Err(e) = send_member_event(&msg, room.clone(), sender).await { + if let Err(e) = send_member_event(&msg, room.clone()).await { warn!("Could not send room member event: {}", e.to_string()); } } - - // finally, send a tick event to force a render - sender.send(Tick).expect("could not send click event") }); } - pub fn send_text_message(&self, room: Joined, message: String) { + pub fn open_content(&self, message: MessageType) { let matrix = self.clone(); self.rt.spawn(async move { - matrix.send(ProgressStarted("Sending message.".to_string())); + Matrix::send(ProgressStarted("Downloading file.".to_string())); + + let (content_type, request) = match message { + Image(content) => ( + content.info.unwrap().mimetype.unwrap(), + MediaRequest { + source: content.source, + format: MediaFormat::File, + }, + ), + Video(content) => ( + content.info.unwrap().mimetype.unwrap(), + MediaRequest { + source: content.source, + format: MediaFormat::File, + }, + ), + _ => { + Matrix::send(Error("Unknown file type.".to_string())); + return; + } + }; + + let handle = match matrix + .client() + .media() + .get_media_file(&request, &content_type.parse().unwrap(), true) + .await + { + Err(err) => { + Matrix::send(Error(err.to_string())); + return; + } + Ok(mfh) => mfh, + }; + + Matrix::send(ProgressComplete); + + tokio::task::spawn_blocking(move || view_file(handle)); + }); + } + + pub fn send_text_message(&self, room: Joined, message: String) { + self.rt.spawn(async move { + Matrix::send(ProgressStarted("Sending message.".to_string())); if let Err(err) = room .send(RoomMessageEventContent::text_plain(message), None) .await { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); } - matrix.send(ProgressComplete); + Matrix::send(ProgressComplete); }); } pub fn send_reaction(&self, room: Joined, event_id: OwnedEventId, key: String) { - let matrix = self.clone(); - self.rt.spawn(async move { - matrix.send(ProgressStarted("Sending reaction.".to_string())); + Matrix::send(ProgressStarted("Sending reaction.".to_string())); if let Err(err) = room .send( @@ -316,24 +347,22 @@ impl Matrix { ) .await { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); } - matrix.send(ProgressComplete); + Matrix::send(ProgressComplete); }); } pub fn redact_event(&self, room: Joined, event_id: OwnedEventId) { - let matrix = self.clone(); - self.rt.spawn(async move { - matrix.send(ProgressStarted("Removing.".to_string())); + Matrix::send(ProgressStarted("Removing.".to_string())); if let Err(err) = room.redact(&event_id, None, None).await { - matrix.send(Error(err.to_string())); + Matrix::send(Error(err.to_string())); } - matrix.send(ProgressComplete); + Matrix::send(ProgressComplete); }); } diff --git a/src/spawn.rs b/src/spawn.rs index 9c7fa0e..256f1bc 100644 --- a/src/spawn.rs +++ b/src/spawn.rs @@ -1,4 +1,5 @@ use anyhow::bail; +use matrix_sdk::media::MediaFileHandle; use std::env::var; use std::io::Read; use std::process::Command; @@ -37,3 +38,16 @@ pub fn get_text() -> anyhow::Result> { Ok(Some(contents)) } + +pub fn view_file(handle: MediaFileHandle) -> anyhow::Result<()> { + let status = open::commands(handle.path())[0].status()?; + + // keep the file handle open until the viewer exits + drop(handle); + + if !status.success() { + bail!("Invalid status code.") + } + + Ok(()) +} diff --git a/src/widgets/chat.rs b/src/widgets/chat.rs index 4087bf1..4adcb22 100644 --- a/src/widgets/chat.rs +++ b/src/widgets/chat.rs @@ -3,6 +3,8 @@ use crate::event::{Event, EventHandler}; use crate::handler::Batch; use crate::matrix::matrix::{pad_emoji, Matrix}; use crate::spawn::get_text; +use crate::widgets::chat::MessageType::Image; +use crate::widgets::chat::MessageType::Video; use crate::widgets::react::React; use crate::widgets::EventResult::Consumed; use crate::widgets::{get_margin, EventResult}; @@ -11,8 +13,10 @@ use crossterm::event::{KeyCode, KeyEvent}; use log::info; use matrix_sdk::room::{Joined, RoomMember}; use once_cell::unsync::OnceCell; -use ruma::events::room::message::MessageType::Text; -use ruma::events::room::message::TextMessageEventContent; +use ruma::events::room::message::MessageType::{self, Text}; +use ruma::events::room::message::{ + ImageMessageEventContent, TextMessageEventContent, VideoMessageEventContent, +}; use ruma::events::room::redaction::RoomRedactionEvent; use ruma::events::AnyMessageLikeEvent::Reaction as Rctn; use ruma::events::AnyMessageLikeEvent::RoomMessage; @@ -114,6 +118,12 @@ impl Chat { self.try_fetch_previous(); return Ok(Consumed(Action::Typing)); } + KeyCode::Char('o') | KeyCode::Enter => { + if let Some(message) = &self.selected_message() { + message.open(self.matrix.clone()) + } + return Ok(Consumed(Action::Typing)); + } KeyCode::Char('i') => { handler.park(); let result = get_text(); @@ -351,19 +361,43 @@ impl Eq for OrderedEvent {} // of constant mutation, as opposed to "events", which just come in, in order. pub struct Message { id: OwnedEventId, - body: String, + body: MessageType, sender: String, reactions: Vec, } impl Message { + fn display(&self) -> &str { + match &self.body { + Text(TextMessageEventContent { body, .. }) => body, + Image(ImageMessageEventContent { body, .. }) => body, + Video(VideoMessageEventContent { body, .. }) => body, + _ => "unknown", + } + } + + fn style(&self) -> Style { + match &self.body { + Text(_) => Style::default(), + _ => Style::default().fg(Color::Blue), + } + } + + fn open(&self, matrix: Matrix) { + match &self.body { + Image(_) => matrix.open_content(self.body.clone()), + Video(_) => matrix.open_content(self.body.clone()), + _ => {} + } + } + // can we make a brand-new message, just from this event? fn try_from(event: &AnyTimelineEvent) -> Option { if let MessageLike(RoomMessage(MessageLikeEvent::Original(c))) = event { let c = c.clone(); let body = match c.content.msgtype { - Text(TextMessageEventContent { body, .. }) => body, + Text(_) | Image(_) | Video(_) => c.content.msgtype, _ => return None, }; @@ -421,7 +455,11 @@ impl Message { // then look at the messages messages.retain(|m| &m.id != id); + + return; } + + info!("{:?}", event); } fn update_senders(&mut self, map: &HashMap) { @@ -450,10 +488,10 @@ impl Message { // message let mut lines = Text::from(Spans::from(spans)); - let wrapped = textwrap::wrap(&self.body, width); + let wrapped = textwrap::wrap(&self.display(), width); for l in wrapped { - lines.extend(Text::from(l)) + lines.extend(Text::styled(l, self.style())) } // reactions