@@ -37,6 +37,7 @@ import { IBodyProps } from "./IBodyProps";
3737import { ImageSize , suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize" ;
3838import { MatrixClientPeg } from '../../../MatrixClientPeg' ;
3939import RoomContext , { TimelineRenderingType } from "../../../contexts/RoomContext" ;
40+ import { blobIsAnimated , mayBeAnimated } from '../../../utils/Image' ;
4041
4142enum Placeholder {
4243 NoImage ,
@@ -59,11 +60,6 @@ interface IState {
5960 placeholder : Placeholder ;
6061}
6162
62- function mayBeAnimated ( mimeType : string ) : boolean {
63- // Both GIF and WEBP can be animated, and here we assume they are, as checking is much more difficult.
64- return [ "image/gif" , "image/webp" ] . includes ( mimeType ) ;
65- }
66-
6763@replaceableComponent ( "views.messages.MImageBody" )
6864export default class MImageBody extends React . Component < IBodyProps , IState > {
6965 static contextType = RoomContext ;
@@ -185,22 +181,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
185181 this . setState ( { imgLoaded : true , loadedImageDimensions } ) ;
186182 } ;
187183
188- protected getContentUrl ( ) : string {
189- const content : IMediaEventContent = this . props . mxEvent . getContent ( ) ;
184+ private getContentUrl ( ) : string {
190185 // During export, the content url will point to the MSC, which will later point to a local url
191- if ( this . props . forExport ) return content . url || content . file ?. url ;
192- if ( this . media . isEncrypted ) {
193- return this . state . contentUrl ;
194- } else {
195- return this . media . srcHttp ;
196- }
186+ if ( this . props . forExport ) return this . media . srcMxc ;
187+ return this . media . srcHttp ;
197188 }
198189
199190 private get media ( ) : Media {
200191 return mediaFromContent ( this . props . mxEvent . getContent ( ) ) ;
201192 }
202193
203- protected getThumbUrl ( ) : string {
194+ private getThumbUrl ( ) : string {
204195 // FIXME: we let images grow as wide as you like, rather than capped to 800x600.
205196 // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
206197 // thumbnail resolution will be unnecessarily reduced.
@@ -260,7 +251,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
260251 }
261252
262253 private async downloadImage ( ) {
263- if ( this . state . contentUrl || this . state . thumbUrl ) return ; // already downloaded
254+ if ( this . state . contentUrl ) return ; // already downloaded
264255
265256 let thumbUrl : string ;
266257 let contentUrl : string ;
@@ -282,26 +273,30 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
282273 }
283274
284275 const content = this . props . mxEvent . getContent < IMediaEventContent > ( ) ;
285- const isAnimated = mayBeAnimated ( content . info ?. mimetype ) ;
276+ let isAnimated = mayBeAnimated ( content . info ?. mimetype ) ;
286277
287278 // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
288279 // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
289280 if ( isAnimated && ! SettingsStore . getValue ( "autoplayGifs" ) ) {
290281 if ( ! thumbUrl || ! content ?. info . thumbnail_info || mayBeAnimated ( content . info . thumbnail_info . mimetype ) ) {
291282 const img = document . createElement ( "img" ) ;
292283 const loadPromise = new Promise ( ( resolve , reject ) => {
293- img . onload = function ( ) {
294- resolve ( img ) ;
295- } ;
296- img . onerror = function ( e ) {
297- reject ( e ) ;
298- } ;
284+ img . onload = resolve ;
285+ img . onerror = reject ;
299286 } ) ;
300287 img . crossOrigin = "Anonymous" ; // CORS allow canvas access
301288 img . src = contentUrl ;
302289
303290 await loadPromise ;
304291
292+ // Rudimentary validation for whether it is animated, only in encrypted rooms as we have the blob
293+ if ( this . props . mediaEventHelper . media . isEncrypted ) {
294+ const blob = await this . props . mediaEventHelper . sourceBlob . value ;
295+ if ( ! await blobIsAnimated ( content . info . mimetype , blob ) ) {
296+ isAnimated = false ;
297+ }
298+ }
299+
305300 if ( isAnimated ) {
306301 const thumb = await createThumbnail ( img , img . width , img . height , content . info . mimetype , false ) ;
307302 thumbUrl = URL . createObjectURL ( thumb . thumbnail ) ;
@@ -370,6 +365,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
370365 content : IMediaEventContent ,
371366 forcedHeight ?: number ,
372367 ) : JSX . Element {
368+ if ( ! thumbUrl ) thumbUrl = contentUrl ; // fallback
369+
373370 let infoWidth : number ;
374371 let infoHeight : number ;
375372
@@ -571,7 +568,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
571568 if ( this . props . forExport || ( this . state . isAnimated && SettingsStore . getValue ( "autoplayGifs" ) ) ) {
572569 thumbUrl = contentUrl ;
573570 } else {
574- thumbUrl = this . state . thumbUrl ;
571+ thumbUrl = this . state . thumbUrl ?? this . state . contentUrl ;
575572 }
576573
577574 const thumbnail = this . messageContent ( contentUrl , thumbUrl , content ) ;
0 commit comments