@ -1,11 +1,6 @@
use adw ::{ prelude ::* , subclass ::prelude ::* } ;
use gettextrs ::gettext ;
use gtk ::{
gdk ,
glib ::{ self , clone } ,
CompositeTemplate ,
} ;
use matrix_sdk ::Client ;
use gtk ::{ gdk , glib , glib ::clone , CompositeTemplate } ;
use ruma ::api ::client ::media ::get_content_thumbnail ::v3 ::Method ;
use tracing ::{ error , warn } ;
@ -13,15 +8,15 @@ use super::{content::MessageCacheKey, ContentFormat};
use crate ::{
components ::{ AnimatedImagePaintable , VideoPlayer } ,
gettext_f ,
session ::model ::Session ,
session ::model ::Room ,
spawn ,
utils ::{
matrix ::VisualMediaMessage ,
matrix ::{ VisualMediaMessage , VisualMediaType } ,
media ::{
image ::{ ImageRequestPriority , ThumbnailSettings , THUMBNAIL_MAX_DIMENSIONS } ,
FrameDimensions ,
} ,
CountedRef , File , LoadingState ,
CountedRef , File , LoadingState , TemplateCallbacks ,
} ,
} ;
@ -35,6 +30,8 @@ const MAX_COMPACT_DIMENSIONS: FrameDimensions = FrameDimensions {
width : 75 ,
height : 50 ,
} ;
/// The name of the placeholder stack page.
const PLACEHOLDER_PAGE : & str = "placeholder" ;
/// The name of the media stack page.
const MEDIA_PAGE : & str = "media" ;
@ -56,11 +53,26 @@ mod imp {
#[ template_child ]
stack : TemplateChild < gtk ::Stack > ,
#[ template_child ]
preview_instructions : TemplateChild < gtk ::Box > ,
#[ template_child ]
preview_instructions_icon : TemplateChild < gtk ::Image > ,
#[ template_child ]
spinner : TemplateChild < adw ::Spinner > ,
#[ template_child ]
hide_preview_button : TemplateChild < gtk ::Button > ,
#[ template_child ]
error : TemplateChild < gtk ::Image > ,
/// The supposed dimensions of the media.
dimensions : Cell < Option < FrameDimensions > > ,
/// The room where the message was sent.
room : glib ::WeakRef < Room > ,
join_rule_handler : RefCell < Option < glib ::SignalHandlerId > > ,
session_settings_handler : RefCell < Option < glib ::SignalHandlerId > > ,
/// The visual media message to display.
media_message : RefCell < Option < VisualMediaMessage > > ,
/// The cache key for the current media message.
///
/// We only try to reload the media if the key changes. This is to avoid
/// reloading the media when a local echo changes to a remote echo.
cache_key : RefCell < MessageCacheKey > ,
/// The loading state of the media.
#[ property(get, builder(LoadingState::default())) ]
state : Cell < LoadingState > ,
@ -74,11 +86,6 @@ mod imp {
#[ property(get) ]
activatable : Cell < bool > ,
gesture_click : glib ::WeakRef < gtk ::GestureClick > ,
/// The cache key for the current media message.
///
/// We only try to reload the media if the key changes. This is to avoid
/// reloading the media when a local echo changes to a remote echo.
cache_key : RefCell < MessageCacheKey > ,
/// The current video file, if any.
file : RefCell < Option < File > > ,
paintable_animation_ref : RefCell < Option < CountedRef > > ,
@ -92,6 +99,8 @@ mod imp {
fn class_init ( klass : & mut Self ::Class ) {
Self ::bind_template ( klass ) ;
Self ::bind_template_callbacks ( klass ) ;
TemplateCallbacks ::bind_template_callbacks ( klass ) ;
klass . set_css_name ( "message-visual-media" ) ;
klass . set_accessible_role ( gtk ::AccessibleRole ::Group ) ;
@ -105,6 +114,7 @@ mod imp {
#[ glib::derived_properties ]
impl ObjectImpl for MessageVisualMedia {
fn dispose ( & self ) {
self . clear ( ) ;
self . overlay . unparent ( ) ;
}
}
@ -171,7 +181,12 @@ mod imp {
} ;
// Use the size from the info or the fallback size.
let media_size = self . dimensions . get ( ) . unwrap_or ( FALLBACK_DIMENSIONS ) ;
let media_size = self
. media_message
. borrow ( )
. as_ref ( )
. and_then ( VisualMediaMessage ::dimensions )
. unwrap_or ( FALLBACK_DIMENSIONS ) ;
let nat = media_size
. scale_to_fit ( wanted_size , gtk ::ContentFit ::ScaleDown )
. dimension_for_orientation ( orientation )
@ -200,6 +215,7 @@ mod imp {
}
}
#[ gtk::template_callbacks ]
impl MessageVisualMedia {
/// The media child of the given type, if any.
pub ( super ) fn media_child < T : IsA < gtk ::Widget > > ( & self ) -> Option < T > {
@ -209,12 +225,14 @@ mod imp {
/// Set the media child.
///
/// Removes the previous media child if one was set.
fn set_media_child ( & self , child : & impl IsA < gtk ::Widget > ) {
fn set_media_child ( & self , child : Option < & impl IsA < gtk ::Widget > > ) {
if let Some ( prev_child ) = self . stack . child_by_name ( MEDIA_PAGE ) {
self . stack . remove ( & prev_child ) ;
}
self . stack . add_named ( child , Some ( MEDIA_PAGE ) ) ;
if let Some ( child ) = child {
self . stack . add_named ( child , Some ( MEDIA_PAGE ) ) ;
}
}
/// Set the state of the media.
@ -223,27 +241,42 @@ mod imp {
return ;
}
match state {
LoadingState ::Loading | LoadingState ::Initial = > {
self . stack . set_visible_child_name ( "placeholder" ) ;
self . spinner . set_visible ( true ) ;
self . error . set_visible ( false ) ;
}
LoadingState ::Ready = > {
self . stack . set_visible_child_name ( MEDIA_PAGE ) ;
self . spinner . set_visible ( false ) ;
self . error . set_visible ( false ) ;
}
LoadingState ::Error = > {
self . spinner . set_visible ( false ) ;
self . error . set_visible ( true ) ;
}
}
self . state . set ( state ) ;
self . update_visible_page ( ) ;
self . obj ( ) . notify_state ( ) ;
}
/// Update the visible page for the current state.
fn update_visible_page ( & self ) {
let Some ( room ) = self . room . upgrade ( ) else {
return ;
} ;
let Some ( session ) = room . session ( ) else {
return ;
} ;
let state = self . state . get ( ) ;
self . preview_instructions
. set_visible ( state = = LoadingState ::Initial ) ;
self . spinner . set_visible ( state = = LoadingState ::Loading ) ;
self . hide_preview_button . set_visible (
state = = LoadingState ::Ready
& & ! session . settings ( ) . should_room_show_media_previews ( & room ) ,
) ;
self . error . set_visible ( state = = LoadingState ::Error ) ;
let visible_page = match state {
LoadingState ::Initial | LoadingState ::Loading = > Some ( PLACEHOLDER_PAGE ) ,
LoadingState ::Ready = > Some ( MEDIA_PAGE ) ,
LoadingState ::Error = > None ,
} ;
if let Some ( visible_page ) = visible_page {
self . stack . set_visible_child_name ( visible_page ) ;
}
}
/// Update the state of the animated paintable, if any.
fn update_animated_paintable_state ( & self ) {
self . paintable_animation_ref . take ( ) ;
@ -276,6 +309,13 @@ mod imp {
self . overlay . remove_css_class ( "compact" ) ;
}
let icon_size = if compact {
gtk ::IconSize ::Normal
} else {
gtk ::IconSize ::Large
} ;
self . preview_instructions_icon . set_icon_size ( icon_size ) ;
self . update_gesture_click ( ) ;
self . obj ( ) . notify_compact ( ) ;
}
@ -304,7 +344,9 @@ mod imp {
#[ weak(rename_to = imp) ]
self ,
move | _ , _ , _ , _ | {
if imp
if imp . state . get ( ) = = LoadingState ::Initial {
imp . show_media ( ) ;
} else if imp
. obj ( )
. activate_action ( "message-row.show-media" , None )
. is_err ( )
@ -333,11 +375,11 @@ mod imp {
should_reload
}
/// Build the content for the given media message .
pub ( super ) fn build (
/// Set the visual media message to display .
pub ( super ) fn set_media_message (
& self ,
media_message : VisualMediaMessage ,
session : & Session ,
room : & Room ,
format : ContentFormat ,
cache_key : MessageCacheKey ,
) {
@ -346,44 +388,136 @@ mod imp {
return ;
}
self . file . take ( ) ;
self . dimensions . set ( media_message . dimensions ( ) ) ;
// Reset the widget.
self . clear ( ) ;
self . set_state ( LoadingState ::Initial ) ;
let compact = matches! ( format , ContentFormat ::Compact | ContentFormat ::Ellipsized ) ;
self . set_compact ( compact ) ;
let activatable = matches! (
media_message ,
VisualMediaMessage ::Image ( _ ) | VisualMediaMessage ::Video ( _ )
) ;
self . set_activatable ( activatable ) ;
let Some ( session ) = room . session ( ) else {
return ;
} ;
let join_rule_handler = room . join_rule ( ) . connect_anyone_can_join_notify ( clone ! (
#[ weak(rename_to = imp) ]
self ,
move | _ | {
imp . update_media ( ) ;
}
) ) ;
self . join_rule_handler . replace ( Some ( join_rule_handler ) ) ;
let session_settings_handler = session
. settings ( )
. connect_media_previews_enabled_changed ( clone ! (
#[ weak(rename_to = imp) ]
self ,
move | _ | {
imp . update_media ( ) ;
}
) ) ;
self . session_settings_handler
. replace ( Some ( session_settings_handler ) ) ;
self . room . set ( Some ( room ) ) ;
self . media_message . replace ( Some ( media_message ) ) ;
self . update_accessible_label ( ) ;
self . update_preview_instructions_icon ( ) ;
self . update_media ( ) ;
}
/// Update the accessible label for the current state.
fn update_accessible_label ( & self ) {
let Some ( ( filename , visual_media_type ) ) =
self . media_message . borrow ( ) . as_ref ( ) . map ( | media_message | {
( media_message . filename ( ) , media_message . visual_media_type ( ) )
} )
else {
return ;
} ;
let filename = media_message . filename ( ) ;
let accessible_label = if filename . is_empty ( ) {
match & media_message {
VisualMediaMessage ::Image ( _ ) = > gettext ( "Image" ) ,
VisualMediaMessage ::Sticker ( _ ) = > gettext ( "Sticker" ) ,
VisualMediaMessage ::Video ( _ ) = > gettext ( "Video" ) ,
match visual_media_typ e {
VisualMediaType ::Image = > gettext ( "Image" ) ,
VisualMediaType ::Sticker = > gettext ( "Sticker" ) ,
VisualMediaType ::Video = > gettext ( "Video" ) ,
}
} else {
match & media_message {
VisualMediaMessage ::Image ( _ ) = > {
match visual_media_typ e {
VisualMediaType ::Image = > {
gettext_f ( "Image: {filename}" , & [ ( "filename" , & filename ) ] )
}
VisualMediaMessage ::Sticker ( _ ) = > {
VisualMediaType ::Sticker = > {
gettext_f ( "Sticker: {filename}" , & [ ( "filename" , & filename ) ] )
}
VisualMediaMessage ::Video ( _ ) = > {
VisualMediaType ::Video = > {
gettext_f ( "Video: {filename}" , & [ ( "filename" , & filename ) ] )
}
}
} ;
self . obj ( )
. update_property ( & [ gtk ::accessible ::Property ::Label ( & accessible_label ) ] ) ;
}
/// Update the preview instructions icon for the current state.
fn update_preview_instructions_icon ( & self ) {
let Some ( content_type ) = self
. media_message
. borrow ( )
. as_ref ( )
. map ( VisualMediaMessage ::content_type )
else {
return ;
} ;
self . preview_instructions_icon
. set_icon_name ( Some ( content_type . icon_name ( ) ) ) ;
}
/// Update the media for the current state.
fn update_media ( & self ) {
let Some ( room ) = self . room . upgrade ( ) else {
return ;
} ;
let Some ( session ) = room . session ( ) else {
return ;
} ;
if session . settings ( ) . should_room_show_media_previews ( & room ) {
// Only load the media if it was not loaded before.
if self . state . get ( ) = = LoadingState ::Initial {
self . show_media ( ) ;
}
} else {
self . hide_media ( ) ;
}
}
/// Hide the media.
#[ template_callback ]
fn hide_media ( & self ) {
self . set_state ( LoadingState ::Initial ) ;
self . set_media_child ( None ::< & gtk ::Widget > ) ;
self . file . take ( ) ;
self . set_activatable ( true ) ;
}
/// Show the media.
fn show_media ( & self ) {
let Some ( media_message ) = self . media_message . borrow ( ) . clone ( ) else {
return ;
} ;
self . set_state ( LoadingState ::Loading ) ;
let client = session . client ( ) ;
let activatable = matches! (
media_message ,
VisualMediaMessage ::Image ( _ ) | VisualMediaMessage ::Video ( _ )
) ;
self . set_activatable ( activatable ) ;
spawn ! (
glib ::Priority ::LOW ,
clone ! (
@ -392,10 +526,10 @@ mod imp {
async move {
match & media_message {
VisualMediaMessage ::Image ( _ ) | VisualMediaMessage ::Sticker ( _ ) = > {
imp . build_image ( & media_message , client ) . await ;
imp . build_image ( & media_message ) . await ;
}
VisualMediaMessage ::Video ( _ ) = > {
imp . build_video ( media_message , & client ) . await ;
imp . build_video ( media_message ) . await ;
}
}
@ -406,14 +540,27 @@ mod imp {
}
/// Build the content for the image in the given media message.
async fn build_image ( & self , media_message : & VisualMediaMessage , client : Client ) {
async fn build_image ( & self , media_message : & VisualMediaMessage ) {
let Some ( client ) = self
. room
. upgrade ( )
. and_then ( | room | room . session ( ) )
. map ( | session | session . client ( ) )
else {
return ;
} ;
if self . state . get ( ) ! = LoadingState ::Loading {
// Something occurred after the task was spawned, cancel the task.
return ;
}
// Disable the copy-image action while the image is loading.
if matches! ( media_message , VisualMediaMessage ::Image ( _ ) ) {
self . enable_copy_image_action ( false ) ;
}
let scale_factor = self . obj ( ) . scale_factor ( ) ;
let settings = ThumbnailSettings {
dimensions : FrameDimensions ::thumbnail_max_dimensions ( scale_factor ) ,
method : Method ::Scale ,
@ -433,13 +580,18 @@ mod imp {
}
} ;
if self . state . get ( ) ! = LoadingState ::Loading {
// Something occurred while the image was loading, cancel the task.
return ;
}
let child = if let Some ( child ) = self . media_child ::< gtk ::Picture > ( ) {
child
} else {
let child = gtk ::Picture ::builder ( )
. content_fit ( gtk ::ContentFit ::ScaleDown )
. build ( ) ;
self . set_media_child ( & child ) ;
self . set_media_child ( Some ( & child ) ) ;
child
} ;
child . set_paintable ( Some ( & gdk ::Paintable ::from ( image ) ) ) ;
@ -479,8 +631,22 @@ mod imp {
}
/// Build the content for the video in the given media message.
async fn build_video ( & self , media_message : VisualMediaMessage , client : & Client ) {
let file = match media_message . into_tmp_file ( client ) . await {
async fn build_video ( & self , media_message : VisualMediaMessage ) {
let Some ( client ) = self
. room
. upgrade ( )
. and_then ( | room | room . session ( ) )
. map ( | session | session . client ( ) )
else {
return ;
} ;
if self . state . get ( ) ! = LoadingState ::Loading {
// Something occurred after the task was spawned, cancel the task.
return ;
}
let file = match media_message . into_tmp_file ( & client ) . await {
Ok ( file ) = > file ,
Err ( error ) = > {
warn ! ( "Could not retrieve video: {error}" ) ;
@ -489,6 +655,11 @@ mod imp {
}
} ;
if self . state . get ( ) ! = LoadingState ::Loading {
// Something occurred while the video was loading, cancel the task.
return ;
}
let child = if let Some ( child ) = self . media_child ::< VideoPlayer > ( ) {
child
} else {
@ -500,7 +671,7 @@ mod imp {
imp . video_state_changed ( player ) ;
}
) ) ;
self . set_media_child ( & child ) ;
self . set_media_child ( Some ( & child ) ) ;
child
} ;
@ -533,6 +704,23 @@ mod imp {
}
}
}
/// Reset the state of this widget.
fn clear ( & self ) {
self . file . take ( ) ;
if let Some ( room ) = self . room . upgrade ( ) {
if let Some ( handler ) = self . join_rule_handler . take ( ) {
room . join_rule ( ) . disconnect ( handler ) ;
}
if let Some ( handler ) = self . session_settings_handler . take ( ) {
if let Some ( session ) = room . session ( ) {
session . settings ( ) . disconnect ( handler ) ;
}
}
}
}
}
}
@ -548,15 +736,16 @@ impl MessageVisualMedia {
glib ::Object ::new ( )
}
/// Display the given visual media message .
/// Set the visual media message to display .
pub ( crate ) fn set_media_message (
& self ,
media_message : VisualMediaMessage ,
session : & Session ,
room : & Room ,
format : ContentFormat ,
cache_key : MessageCacheKey ,
) {
self . imp ( ) . build ( media_message , session , format , cache_key ) ;
self . imp ( )
. set_media_message ( media_message , room , format , cache_key ) ;
}
/// Get the texture displayed by this widget, if any.