Skip to content

Commit

Permalink
Save files.
Browse files Browse the repository at this point in the history
  • Loading branch information
pkulak committed May 10, 2023
1 parent 9bee6f8 commit c604697
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 46 deletions.
13 changes: 7 additions & 6 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 @@ -24,6 +24,7 @@ notify = "5.1"
once_cell = "1.17"
open = "4.0"
rand = "0.8.5"
regex = "1.8.1"
serde = { version = "1.0", features = ["derive"] }
simple-logging = "2.0"
tempfile = "3"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ complicated. Especially if you don't implement too many features.
| k* | Select one line up. |
| i | Create a new message using the external editor. |
| Enter | Open the selected message (images, videos, urls, etc). |
| s | Save the selected message (images and videos). |
| c | Edit the selected message in the external editor. |
| r | React to the selected message. |
| R | Reply to the selected message. |
Expand Down
12 changes: 8 additions & 4 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use ruma::events::AnyTimelineEvent;

#[derive(Clone, Debug)]
pub enum MatuiEvent {
Confirm(String, String),
Error(String),
LoginComplete,
LoginRequired,
Expand Down Expand Up @@ -50,6 +51,9 @@ pub struct Batch {

pub fn handle_app_event(event: MatuiEvent, app: &mut App) {
match event {
MatuiEvent::Confirm(header, msg) => {
app.set_popup(Popup::Error(Error::with_heading(header, msg)));
}
MatuiEvent::Error(msg) => {
app.set_popup(Popup::Error(Error::new(msg)));
}
Expand All @@ -62,17 +66,17 @@ pub fn handle_app_event(event: MatuiEvent, app: &mut App) {
MatuiEvent::LoginComplete => {
app.popup = None;
}
MatuiEvent::ProgressStarted(msg, delay) => {
app.set_popup(Popup::Progress(Progress::new(&msg, delay)))
}
MatuiEvent::ProgressComplete => app.popup = None,

// Let the chat update when we learn about room membership
MatuiEvent::RoomMember(room, member) => {
if let Some(c) = &mut app.chat {
c.room_member_event(room, member);
}
}
MatuiEvent::ProgressStarted(msg, delay) => {
app.set_popup(Popup::Progress(Progress::new(&msg, delay)))
}
MatuiEvent::ProgressComplete => app.popup = None,
MatuiEvent::RoomSelected(room) => app.select_room(room),
MatuiEvent::SyncStarted(st) => {
match st {
Expand Down
27 changes: 23 additions & 4 deletions src/matrix/matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ use crate::handler::MatuiEvent::{
};
use crate::handler::{Batch, MatuiEvent, SyncType};
use crate::matrix::roomcache::{DecoratedRoom, RoomCache};
use crate::spawn::view_file;
use crate::spawn::{save_file, view_file};

use super::mime::mime_from_path;
use super::notify::Notify;
Expand All @@ -65,6 +65,12 @@ pub struct Matrix {
notify: Arc<Notify>,
}

/// What should we do with the file after we download it?
pub enum AfterDownload {
View,
Save,
}

impl Default for Matrix {
fn default() -> Self {
let rt = tokio::runtime::Builder::new_multi_thread()
Expand Down Expand Up @@ -305,26 +311,28 @@ impl Matrix {
});
}

pub fn open_content(&self, message: MessageType) {
pub fn download_content(&self, message: MessageType, after: AfterDownload) {
let matrix = self.clone();

self.rt.spawn(async move {
Matrix::send(ProgressStarted("Downloading file.".to_string(), 250));

let (content_type, request) = match message {
let (content_type, request, file_name) = match message {
Image(content) => (
content.info.unwrap().mimetype.unwrap(),
MediaRequest {
source: content.source,
format: MediaFormat::File,
},
content.body,
),
Video(content) => (
content.info.unwrap().mimetype.unwrap(),
MediaRequest {
source: content.source,
format: MediaFormat::File,
},
content.body,
),
_ => {
Matrix::send(Error("Unknown file type.".to_string()));
Expand All @@ -347,7 +355,18 @@ impl Matrix {

Matrix::send(ProgressComplete);

tokio::task::spawn_blocking(move || view_file(handle));
match after {
AfterDownload::View => {
tokio::task::spawn_blocking(move || view_file(handle));
}
AfterDownload::Save => match save_file(handle, &file_name) {
Err(err) => Matrix::send(Error(err.to_string())),
Ok(path) => Matrix::send(MatuiEvent::Confirm(
"Download Complete".to_string(),
format!("Saved to {}", path.to_str().unwrap()),
)),
},
};
});
}

Expand Down
73 changes: 73 additions & 0 deletions src/spawn.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
use anyhow::{bail, Context};
use image::imageops::FilterType;
use lazy_static::lazy_static;
use linkify::LinkFinder;
use log::error;
use matrix_sdk::media::MediaFileHandle;
use native_dialog::FileDialog;
use notify_rust::Hint;
use regex::Regex;
use std::env::var;
use std::fs;
use std::io::{Cursor, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tempfile::Builder;

lazy_static! {
static ref FILE_RE: Regex = Regex::new(r"-([0-9]+)(\.|$)").unwrap();
}

pub fn get_file_paths() -> anyhow::Result<Vec<PathBuf>> {
let home = dirs::home_dir().context("no home directory")?;

Expand Down Expand Up @@ -93,6 +100,46 @@ pub fn view_file(handle: MediaFileHandle) -> anyhow::Result<()> {
Ok(())
}

pub fn save_file(handle: MediaFileHandle, file_name: &str) -> anyhow::Result<PathBuf> {
let mut destination = dirs::download_dir().context("no download directory")?;
destination.push(file_name);
let destination = make_unique(destination);
fs::copy(handle.path(), &destination)?;
Ok(destination)
}

pub fn make_unique(mut path: PathBuf) -> PathBuf {
loop {
if !path.exists() {
return path;
}

path.set_file_name(next_file_name(
path.file_name().expect("no file name").to_str().unwrap(),
));
}
}

fn next_file_name(og: &str) -> String {
// if there's already a version, increment
if let Some(cap) = FILE_RE.captures_iter(og).next() {
if let Ok(version) = cap[1].parse::<usize>() {
let replacement = format!("-{}$2", version + 1);
return FILE_RE.replace(og, replacement).to_string();
}
}

// if there's an extension, start a new version just before
if og.contains('.') {
let reversed: String = og.chars().rev().collect();
let replaced = reversed.replacen('.', ".1-", 1);
return replaced.chars().rev().collect();
}

// otherwise, just throw it on the end
format!("{}-1", og)
}

pub fn view_text(text: &str) {
let finder = LinkFinder::new();

Expand Down Expand Up @@ -127,3 +174,29 @@ pub fn send_notification(summary: &str, body: &str, image: Option<Vec<u8>>) -> a

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_next_file_first() {
assert_eq!(next_file_name("image.jpg"), "image-1.jpg");
}

#[test]
fn test_next_file_second() {
assert_eq!(next_file_name("image-1.jpg"), "image-2.jpg");
}

#[test]
fn test_next_file_too_many() {
assert_eq!(next_file_name("image-375.jpg"), "image-376.jpg");
}

#[test]
fn test_next_no_ext() {
assert_eq!(next_file_name("image"), "image-1");
assert_eq!(next_file_name("image-42"), "image-43");
}
}
52 changes: 38 additions & 14 deletions src/widgets/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use log::info;
use matrix_sdk::room::{Joined, RoomMember};
use once_cell::sync::OnceCell;
use ruma::events::receipt::ReceiptEventContent;
use ruma::events::room::member::MembershipState;
use ruma::events::room::message::MessageType::Text;
use ruma::events::AnyTimelineEvent;
use ruma::{OwnedEventId, OwnedUserId};
Expand Down Expand Up @@ -175,6 +174,12 @@ impl Chat {
}
Ok(consumed!())
}
KeyCode::Char('s') => {
if let Some(message) = &self.selected_reply() {
message.save(self.matrix.clone())
}
Ok(consumed!())
}
KeyCode::Char('c') => {
let message = match self.selected_reply() {
Some(m) => m,
Expand Down Expand Up @@ -345,6 +350,7 @@ impl Chat {
self.check_event_sender(&event);
self.events.insert(OrderedEvent::new(event));
self.messages = make_message_list(&self.events, &self.members, &self.receipts);
self.pretty_members = OnceCell::new();
self.set_fully_read();
}

Expand Down Expand Up @@ -388,6 +394,7 @@ impl Chat {
if joined.room_id() == self.room.room_id() {
self.receipts.apply_event(content);
self.messages = make_message_list(&self.events, &self.members, &self.receipts);
self.pretty_members = OnceCell::new();

// make sure we fetch any users we don't know about
for id in Receipts::get_senders(content) {
Expand Down Expand Up @@ -429,9 +436,7 @@ impl Chat {
}

fn check_event_sender(&mut self, event: &AnyTimelineEvent) {
if let Some(user_id) = Message::get_sender(event) {
self.check_sender(user_id)
}
self.check_sender(&event.sender().to_owned());
}

fn check_sender(&mut self, user_id: &OwnedUserId) {
Expand Down Expand Up @@ -495,27 +500,46 @@ impl Chat {

fn pretty_members(&self) -> &str {
self.pretty_members.get_or_init(|| {
let mut names: Vec<&str> = self
.members
let mut members: Vec<&RoomMember> = vec![];

// first grab folks who have sent read receipts
let mut receipts = self.receipts.get_all();

while let Some(receipt) = receipts.pop() {
if let Some(member) = self.members.iter().find(|m| m.user_id() == receipt.user_id) {
members.push(member);
}
}

// then walk all the events backwards, until we have a decent number
for event in self.events.iter().rev() {
if members.iter().any(|m| m.user_id() == event.sender()) {
continue;
}

if let Some(member) = self.members.iter().find(|m| m.user_id() == event.sender()) {
members.push(member);
}

if members.len() > 5 {
break;
}
}

let names: Vec<&str> = members
.iter()
.filter(|m| m.membership() == &MembershipState::Join)
.map(|m| {
m.display_name()
.or_else(|| Some(m.user_id().localpart()))
.unwrap()
.unwrap_or_else(|| m.user_id().localpart())
.split_whitespace()
.next()
.unwrap_or_default()
})
.collect();

names.sort();
names.dedup();

let total = names.len();
let iter = names.into_iter().map(|n| n.to_string());

pretty_list(limit_list(iter, 5, total, Some("at least")))
pretty_list(limit_list(iter, 5, self.members.len(), Some("at least")))
})
}

Expand Down
Loading

0 comments on commit c604697

Please sign in to comment.