mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 05:39:52 -07:00
Add support for previewing images in room scrollback (#108)
This commit is contained in:
parent
974775b29b
commit
221faa828d
15 changed files with 588 additions and 29 deletions
172
src/preview.rs
Normal file
172
src/preview.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
media::{MediaFormat, MediaRequest},
|
||||
ruma::{
|
||||
events::{
|
||||
room::{
|
||||
message::{MessageType, RoomMessageEventContent},
|
||||
MediaSource,
|
||||
},
|
||||
MessageLikeEvent,
|
||||
},
|
||||
OwnedEventId,
|
||||
OwnedRoomId,
|
||||
},
|
||||
Media,
|
||||
};
|
||||
use modalkit::tui::layout::Rect;
|
||||
use ratatui_image::Resize;
|
||||
|
||||
use crate::{
|
||||
base::{AsyncProgramStore, ChatStore, IambError},
|
||||
config::ImagePreviewSize,
|
||||
message::ImageStatus,
|
||||
};
|
||||
|
||||
pub fn source_from_event(
|
||||
ev: &MessageLikeEvent<RoomMessageEventContent>,
|
||||
) -> Option<(OwnedEventId, MediaSource)> {
|
||||
if let MessageLikeEvent::Original(ev) = &ev {
|
||||
if let MessageType::Image(c) = &ev.content.msgtype {
|
||||
return Some((ev.event_id.clone(), c.source.clone()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl From<ImagePreviewSize> for Rect {
|
||||
fn from(value: ImagePreviewSize) -> Self {
|
||||
Rect::new(0, 0, value.width as _, value.height as _)
|
||||
}
|
||||
}
|
||||
impl From<Rect> for ImagePreviewSize {
|
||||
fn from(rect: Rect) -> Self {
|
||||
ImagePreviewSize { width: rect.width as _, height: rect.height as _ }
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and prepare the preview, and then lock the store to insert it.
|
||||
pub fn spawn_insert_preview(
|
||||
store: AsyncProgramStore,
|
||||
room_id: OwnedRoomId,
|
||||
event_id: OwnedEventId,
|
||||
source: MediaSource,
|
||||
media: Media,
|
||||
cache_dir: PathBuf,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let img = download_or_load(event_id.to_owned(), source, media, cache_dir)
|
||||
.await
|
||||
.map(std::io::Cursor::new)
|
||||
.map(image::io::Reader::new)
|
||||
.map_err(IambError::Matrix)
|
||||
.and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError))
|
||||
.and_then(|reader| reader.decode().map_err(IambError::Image));
|
||||
|
||||
match img {
|
||||
Err(err) => {
|
||||
try_set_msg_preview_error(
|
||||
&mut store.lock().await.application,
|
||||
room_id,
|
||||
event_id,
|
||||
err,
|
||||
);
|
||||
},
|
||||
Ok(img) => {
|
||||
let mut locked = store.lock().await;
|
||||
let ChatStore { rooms, picker, settings, .. } = &mut locked.application;
|
||||
|
||||
match picker
|
||||
.as_mut()
|
||||
.ok_or_else(|| IambError::Preview("Picker is empty".to_string()))
|
||||
.and_then(|picker| {
|
||||
Ok((
|
||||
picker,
|
||||
rooms
|
||||
.get_or_default(room_id.clone())
|
||||
.get_event_mut(&event_id)
|
||||
.ok_or_else(|| {
|
||||
IambError::Preview("Message not found".to_string())
|
||||
})?,
|
||||
settings.tunables.image_preview.clone().ok_or_else(|| {
|
||||
IambError::Preview("image_preview settings not found".to_string())
|
||||
})?,
|
||||
))
|
||||
})
|
||||
.and_then(|(picker, msg, image_preview)| {
|
||||
picker
|
||||
.new_protocol(img, image_preview.size.into(), Resize::Fit)
|
||||
.map_err(|err| IambError::Preview(format!("{err:?}")))
|
||||
.map(|backend| (backend, msg))
|
||||
}) {
|
||||
Err(err) => {
|
||||
try_set_msg_preview_error(&mut locked.application, room_id, event_id, err);
|
||||
},
|
||||
Ok((backend, msg)) => {
|
||||
msg.image_preview = ImageStatus::Loaded(backend);
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn try_set_msg_preview_error(
|
||||
application: &mut ChatStore,
|
||||
room_id: OwnedRoomId,
|
||||
event_id: OwnedEventId,
|
||||
err: IambError,
|
||||
) {
|
||||
let rooms = &mut application.rooms;
|
||||
|
||||
match rooms
|
||||
.get_or_default(room_id.clone())
|
||||
.get_event_mut(&event_id)
|
||||
.ok_or_else(|| IambError::Preview("Message not found".to_string()))
|
||||
{
|
||||
Ok(msg) => msg.image_preview = ImageStatus::Error(format!("{err:?}")),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Failed to set error on msg.image_backend for event {}, room {}: {}",
|
||||
event_id,
|
||||
room_id,
|
||||
err
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_or_load(
|
||||
event_id: OwnedEventId,
|
||||
source: MediaSource,
|
||||
media: Media,
|
||||
mut cache_path: PathBuf,
|
||||
) -> Result<Vec<u8>, matrix_sdk::Error> {
|
||||
cache_path.push(Path::new(event_id.localpart()));
|
||||
|
||||
match File::open(&cache_path) {
|
||||
Ok(mut f) => {
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
},
|
||||
Err(_) => {
|
||||
media
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.await
|
||||
.and_then(|buffer| {
|
||||
if let Err(err) =
|
||||
File::create(&cache_path).and_then(|mut f| f.write_all(&buffer))
|
||||
{
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(buffer)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue