Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch and display image thumbnails in the timeline instead of the full-resolution image #322

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
93 changes: 49 additions & 44 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ live_design! {
use crate::shared::typing_animation::TypingAnimation;
use crate::shared::icon_button::*;
use crate::shared::jump_to_bottom_button::*;
use crate::shared::image_viewer::ImageViewer;
use crate::home::loading_modal::*;
use crate::home::message_context_menu::*;

Expand Down Expand Up @@ -688,6 +689,9 @@ live_design! {
draw_bg: {
color: (COLOR_PRIMARY_DARKER)
}
image_viewer = <ImageViewer> {

}

keyboard_view = <KeyboardView> {
width: Fill, height: Fill,
Expand Down Expand Up @@ -1546,7 +1550,7 @@ impl RoomScreen {
submit_async_request(MatrixRequest::GetNumberUnreadMessages{ room_id: room_id.clone() });
}
}

if clear_cache {
tl.content_drawn_since_last_update.clear();
tl.profile_drawn_since_last_update.clear();
Expand Down Expand Up @@ -2147,7 +2151,7 @@ impl RoomScreen {
event_id: last_event_id.to_owned(),
});
}

}
}
}
Expand Down Expand Up @@ -3075,51 +3079,52 @@ fn populate_image_message_content(
}
}

match image_info_source.map(|(_, source)| source) {
Some(MediaSource::Plain(mxc_uri)) => {
// now that we've obtained the image URI and its metadata, try to fetch the image.
match media_cache.try_get_media_or_fetch(mxc_uri.clone(), None) {
MediaCacheEntry::Loaded(data) => {
let show_image_result = text_or_image_ref.show_image(|img| {
utils::load_png_or_jpg(&img, cx, &data)
.map(|()| img.size_in_pixels(cx).unwrap())
});
if let Err(e) = show_image_result {
let err_str = format!("{body}\n\nFailed to display image: {e:?}");
error!("{err_str}");
text_or_image_ref.show_text(&err_str);
}
match image_info_source.and_then(|(image_info, _)|image_info)
aaravlu marked this conversation as resolved.
Show resolved Hide resolved
.map(|image_info|image_info.thumbnail_source) {
Some(Some(MediaSource::Plain(mxc_uri))) => {
// Now that we've obtained thumbnail of the image URI and its metadata.
// Let's try to fetch it.
match media_cache.try_get_media_or_fetch(mxc_uri.clone(), None) {
MediaCacheEntry::Loaded(data) => {
let show_image_result = text_or_image_ref.show_image(|img| {
utils::load_png_or_jpg(&img, cx, &data)
.map(|()| img.size_in_pixels(cx).unwrap_or_default())
});
if let Err(e) = show_image_result {
let err_str = format!("{body}\n\nFailed to display image: {e:?}");
error!("{err_str}");
text_or_image_ref.show_text(&err_str);
}

// We're done drawing the image message content, so mark it as fully drawn.
true
}
MediaCacheEntry::Requested => {
text_or_image_ref.show_text(format!("{body}\n\nFetching image from {:?}", mxc_uri));
// Do not consider this image as being fully drawn, as we're still fetching it.
false
}
MediaCacheEntry::Failed => {
text_or_image_ref
.show_text(format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri));
// For now, we consider this as being "complete". In the future, we could support
// retrying to fetch the image on a user click/tap.
true
// We're done drawing thumbnail of the image message content, so mark it as fully drawn.
true
}
MediaCacheEntry::Requested => {
text_or_image_ref.show_text(format!("{body}\n\nFetching image from {:?}", mxc_uri));
// Do not consider this thumbnail as being fully drawn, as we're still fetching it.
false
}
MediaCacheEntry::Failed => {
text_or_image_ref
.show_text(format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri));
// For now, we consider this as being "complete". In the future, we could support
// retrying to fetch thumbnail of the image on a user click/tap.
true
}
}
}
}
Some(MediaSource::Encrypted(encrypted)) => {
text_or_image_ref.show_text(format!(
"{body}\n\n[TODO] fetch encrypted image at {:?}",
encrypted.url
));
// We consider this as "fully drawn" since we don't yet support encryption.
true
}
None => {
text_or_image_ref.show_text("{body}\n\nImage message had no source URL.");
true
}

Some(Some(MediaSource::Encrypted(encrypted))) => {
text_or_image_ref.show_text(format!(
"{body}\n\n[TODO] fetch encrypted image at {:?}",
encrypted.url
));
// We consider this as "fully drawn" since we don't yet support encryption.
true
}
Some(None) | None => {
text_or_image_ref.show_text("{body}\n\nImage message had no source URL.");
true
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/shared/clickable_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,9 @@ impl ClickableViewRef {
}
false
}
pub fn set_visible(&self, visible: bool) {
if let Some(mut inner) = self.borrow_mut() {
inner.visible = visible
}
}
}
122 changes: 122 additions & 0 deletions src/shared/image_viewer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use makepad_widgets::*;

use matrix_sdk::ruma::events::room::{ImageInfo, MediaSource};

use crate::{media_cache::{MediaCache, MediaCacheEntry}, utils};

#[derive(Clone, DefaultNone, Debug)]
pub enum ImageViewerAction {
Open,
None
}

live_design! {
use link::theme::*;
use link::widgets::*;

use crate::shared::styles::*;
use crate::shared::icon_button::RobrixIconButton;

pub ImageViewer = {{ImageViewer}} {
width: Fit, height: Fit
visible: false

flow: Overlay

close_button = <RobrixIconButton> {
align: {x: 1., y: 0.}
enabled: false,
padding: {top: 0, right: 0}
draw_icon: {
svg_file: (ICON_CLOSE)
color: (COLOR_ACCEPT_GREEN),
}
icon_walk: {width: 16, height: 16, margin: {left: -1, right: -1} }

draw_bg: {
border_color: (COLOR_ACCEPT_GREEN),
color: #f0fff0 // light green
}
}
image = <Image> {
fit: Stretch,
width: Fill, height: Fill,
fit: Largest,
// source: (IMG_DEFAULT_AVATAR),
}
}
}

#[derive(Live, LiveHook, Widget)]
pub struct ImageViewer {
#[deref] view: View,
}

impl Widget for ImageViewer {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.match_event(cx, event);
self.view.handle_event(cx, event, scope);
}
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
self.view.draw_walk(cx, scope, walk)
}
}

impl MatchEvent for ImageViewer {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
if self.button(id!(close_button)).clicked(actions) {
log!("")
}

for action in actions {
match action.downcast_ref() {
Some(ImageViewerAction::Open) => {
self.visible = true;
self.redraw(cx);
},
Some(ImageViewerAction::None) | None => {
}
}
}
}
}

impl ImageViewer {
/// We fetch thumbnail of the image in `populate_image_message_content` in `room_screen.rs`.
///
/// We fetch origin of the image and show it here.
pub fn fetch_and_show_image<F, E> (
&mut self,
cx: &mut Cx,
image_info_source: Option<(Option<ImageInfo>, MediaSource)>,
media_cache: &mut MediaCache
)
{
let image_ref = self.view.image(id!(image));

match image_info_source.map(|(_, media_source)| media_source ) {
Some(MediaSource::Plain(mxc_uri)) => {
// Now that we've obtained thumbnail of the image URI and its metadata.
// Let's try to fetch it.
match media_cache.try_get_media_or_fetch(mxc_uri.clone(), None) {
MediaCacheEntry::Loaded(data) => {
let load_image_task = utils::load_png_or_jpg(&image_ref, cx, &data)
.map(|()| image_ref.size_in_pixels(cx).unwrap_or_default());

if let Err(e) = load_image_task {
log!("Image loading error: {e}")
}

}
MediaCacheEntry::Requested | MediaCacheEntry::Failed=> {
// TODO: Show loading spinner.
}
}
}
Some(MediaSource::Encrypted(_encrypted)) => {
}
None => {
}
}
}
}
2 changes: 2 additions & 0 deletions src/shared/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod styles;
pub mod text_or_image;
pub mod typing_animation;
pub mod verification_badge;
pub mod image_viewer;

pub fn live_design(cx: &mut Cx) {
// Order matters here, as some widget definitions depend on others.
Expand All @@ -27,4 +28,5 @@ pub fn live_design(cx: &mut Cx) {
jump_to_bottom_button::live_design(cx);
verification_badge::live_design(cx);
color_tooltip::live_design(cx);
image_viewer::live_design(cx);
}
27 changes: 21 additions & 6 deletions src/shared/text_or_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@

use makepad_widgets::*;

use crate::shared::image_viewer::ImageViewerAction;

use super::clickable_view::ClickableViewWidgetExt;

live_design! {
use link::theme::*;
use link::shaders::*;
use link::widgets::*;

use crate::shared::styles::*;
use crate::shared::clickable_view::ClickableView;

pub TextOrImage = {{TextOrImage}} {
width: Fill, height: Fit,
Expand All @@ -32,9 +37,9 @@ live_design! {
}
}
}
image_view = <View> {
image_view = <ClickableView> {
visible: false,
cursor: NotAllowed, // we don't yet support clicking on the image
cursor: Hand,
width: Fill, height: Fit,
image = <Image> {
width: Fill, height: Fit,
Expand All @@ -44,7 +49,6 @@ live_design! {
}
}


/// A view that holds an image or text content, and can switch between the two.
///
/// This is useful for displaying alternate text when an image is not (yet) available
Expand All @@ -60,21 +64,32 @@ pub struct TextOrImage {

impl Widget for TextOrImage {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.match_event(cx, event);
self.view.handle_event(cx, event, scope);
}

fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
self.view.draw_walk(cx, scope, walk)
}
}

impl MatchEvent for TextOrImage {
fn handle_actions(&mut self, _: &mut Cx, actions: &Actions) {
if self.clickable_view(id!(image_view)).clicked(actions) {
log!("Image clicked");
Cx::post_action(ImageViewerAction::Open);
}
}
}

impl TextOrImage {
/// Sets the text content, which will be displayed on future draw operations.
///
/// ## Arguments
/// * `text`: the text that will be displayed in this `TextOrImage`, e.g.,
/// a message like "Loading..." or an error message.
pub fn show_text<T: AsRef<str>>(&mut self, text: T) {
self.view(id!(image_view)).set_visible(false);
self.clickable_view(id!(image_view)).set_visible(false);
self.view(id!(text_view)).set_visible(true);
self.view.label(id!(text_view.label)).set_text(text.as_ref());
self.status = TextOrImageStatus::Text;
Expand All @@ -97,7 +112,7 @@ impl TextOrImage {
Ok(size_in_pixels) => {
self.status = TextOrImageStatus::Image;
self.size_in_pixels = size_in_pixels;
self.view(id!(image_view)).set_visible(true);
self.clickable_view(id!(image_view)).set_visible(true);
self.view(id!(text_view)).set_visible(false);
Ok(())
}
Expand Down