Add support for previewing images in room scrollback (#108)

This commit is contained in:
Benjamin Grosse 2023-11-16 08:36:22 -08:00 committed by GitHub
parent 974775b29b
commit 221faa828d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 588 additions and 29 deletions

View file

@ -11,6 +11,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use emojis::Emoji;
use ratatui_image::picker::{Picker, ProtocolType};
use serde::{
de::Error as SerdeError,
de::Visitor,
@ -81,6 +82,9 @@ use modalkit::{
},
};
use crate::config::ImagePreviewProtocolValues;
use crate::message::ImageStatus;
use crate::preview::{source_from_event, spawn_insert_preview};
use crate::{
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
worker::Requester,
@ -617,6 +621,14 @@ pub enum IambError {
/// A failure to access the system's clipboard.
#[error("Could not use system clipboard data")]
Clipboard,
/// An failure during disk/network/ipc/etc. I/O.
#[error("Input/Output error: {0}")]
IOError(#[from] std::io::Error),
/// A failure while trying to show an image preview.
#[error("Preview error: {0}")]
Preview(String),
}
impl From<IambError> for UIError<IambInfo> {
@ -737,6 +749,11 @@ impl RoomInfo {
self.messages.get(self.get_message_key(event_id)?)
}
/// Get an event for an identifier as mutable.
pub fn get_event_mut(&mut self, event_id: &EventId) -> Option<&mut Message> {
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
}
/// Insert a reaction to a message.
pub fn insert_reaction(&mut self, react: ReactionEvent) {
match react {
@ -827,6 +844,37 @@ impl RoomInfo {
}
}
/// Insert a new message event, and spawn a task for image-preview if it has an image
/// attachment.
pub fn insert_with_preview(
&mut self,
room_id: OwnedRoomId,
store: AsyncProgramStore,
picker: Option<Picker>,
ev: RoomMessageEvent,
settings: &mut ApplicationSettings,
media: matrix_sdk::Media,
) {
let source = picker.and_then(|_| source_from_event(&ev));
self.insert(ev);
if let Some((event_id, source)) = source {
if let (Some(msg), Some(image_preview)) =
(self.get_event_mut(&event_id), &settings.tunables.image_preview)
{
msg.image_preview = ImageStatus::Downloading(image_preview.size.clone());
spawn_insert_preview(
store,
room_id,
event_id,
source,
media,
settings.dirs.image_previews.clone(),
)
}
}
}
/// Indicates whether we've recently fetched scrollback for this room.
pub fn recently_fetched(&self) -> bool {
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
@ -936,6 +984,51 @@ fn emoji_map() -> CompletionMap<String, &'static Emoji> {
return emojis;
}
#[cfg(unix)]
fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
let mut picker = match Picker::from_termios() {
Ok(picker) => picker,
Err(e) => {
tracing::error!("Failed to setup image previews: {e}");
return None;
},
};
if let Some(protocol_type) = protocol_type {
picker.protocol_type = protocol_type;
} else {
picker.guess_protocol();
}
Some(picker)
}
/// Windows cannot guess the right protocol, and always needs type and font_size.
#[cfg(windows)]
fn picker_from_termios(_: Option<ProtocolType>) -> Option<Picker> {
tracing::error!("\"image_preview\" requires \"protocol\" with \"type\" and \"font_size\" options on Windows.");
None
}
fn picker_from_settings(settings: &ApplicationSettings) -> Option<Picker> {
let image_preview = settings.tunables.image_preview.as_ref()?;
let image_preview_protocol = image_preview.protocol.as_ref();
if let Some(&ImagePreviewProtocolValues {
r#type: Some(protocol_type),
font_size: Some(font_size),
}) = image_preview_protocol
{
// User forced type and font_size: use that.
let mut picker = Picker::new(font_size);
picker.protocol_type = protocol_type;
Some(picker)
} else {
// Guess, but use type if forced.
picker_from_termios(image_preview_protocol.and_then(|p| p.r#type))
}
}
/// Information gathered during server syncs about joined rooms.
#[derive(Default)]
pub struct SyncInfo {
@ -980,15 +1073,20 @@ pub struct ChatStore {
/// Information gathered by the background thread.
pub sync_info: SyncInfo,
/// Image preview "protocol" picker.
pub picker: Option<Picker>,
}
impl ChatStore {
/// Create a new [ChatStore].
pub fn new(worker: Requester, settings: ApplicationSettings) -> Self {
let picker = picker_from_settings(&settings);
ChatStore {
worker,
settings,
picker,
cmds: crate::commands::setup_commands(),
emojis: emoji_map(),