@@ -539,7 +539,7 @@ class ImagePreviewNode extends BlockContentNode {
539539 const ImagePreviewNode ({
540540 super .debugHtmlNode,
541541 required this .srcUrl,
542- required this .thumbnailUrl ,
542+ required this .thumbnail ,
543543 required this .loading,
544544 required this .originalWidth,
545545 required this .originalHeight,
@@ -551,15 +551,15 @@ class ImagePreviewNode extends BlockContentNode {
551551 /// authentication credentials to the request.
552552 final String srcUrl;
553553
554- /// The thumbnail URL of the image.
554+ /// The thumbnail URL of the image and whether it has an animated version .
555555 ///
556- /// This may be a relative URL string. It also may not work without adding
557- /// authentication credentials to the request.
556+ /// [ImageThumbnailLocator.urlPath] is a relative URL string.
557+ /// It may not work without adding authentication credentials to the request.
558558 ///
559559 /// This will be null if the server hasn't yet generated a thumbnail,
560560 /// or is a version that doesn't offer thumbnails.
561561 /// It will also be null when [loading] is true.
562- final String ? thumbnailUrl ;
562+ final ImageThumbnailLocator ? thumbnail ;
563563
564564 /// A flag to indicate whether to show the placeholder.
565565 ///
@@ -576,27 +576,61 @@ class ImagePreviewNode extends BlockContentNode {
576576 bool operator == (Object other) {
577577 return other is ImagePreviewNode
578578 && other.srcUrl == srcUrl
579- && other.thumbnailUrl == thumbnailUrl
579+ && other.thumbnail == thumbnail
580580 && other.loading == loading
581581 && other.originalWidth == originalWidth
582582 && other.originalHeight == originalHeight;
583583 }
584584
585585 @override
586586 int get hashCode => Object .hash ('ImagePreviewNode' ,
587- srcUrl, thumbnailUrl , loading, originalWidth, originalHeight);
587+ srcUrl, thumbnail , loading, originalWidth, originalHeight);
588588
589589 @override
590590 void debugFillProperties (DiagnosticPropertiesBuilder properties) {
591591 super .debugFillProperties (properties);
592592 properties.add (StringProperty ('srcUrl' , srcUrl));
593- properties.add (StringProperty ( 'thumbnailUrl ' , thumbnailUrl ));
593+ properties.add (DiagnosticsProperty < ImageThumbnailLocator >( 'thumbnail ' , thumbnail ));
594594 properties.add (FlagProperty ('loading' , value: loading, ifTrue: "is loading" ));
595595 properties.add (DoubleProperty ('originalWidth' , originalWidth));
596596 properties.add (DoubleProperty ('originalHeight' , originalHeight));
597597 }
598598}
599599
600+ /// Data to locate an image thumbnail,
601+ /// and whether the image has an animated version.
602+ @immutable
603+ class ImageThumbnailLocator extends DiagnosticableTree {
604+ ImageThumbnailLocator ({
605+ required this .urlPath,
606+ required this .hasAnimatedVersion,
607+ }) : assert (urlPath.startsWith (urlPathPrefix));
608+
609+ final String urlPath;
610+ final bool hasAnimatedVersion;
611+
612+ static const urlPathPrefix = '/user_uploads/thumbnail/' ;
613+
614+ @override
615+ bool operator == (Object other) {
616+ if (other is ! ImageThumbnailLocator ) return false ;
617+ return urlPath == other.urlPath
618+ && hasAnimatedVersion == other.hasAnimatedVersion;
619+ }
620+
621+ @override
622+ int get hashCode => Object .hash (urlPath, hasAnimatedVersion);
623+
624+ @override
625+ void debugFillProperties (DiagnosticPropertiesBuilder properties) {
626+ super .debugFillProperties (properties);
627+ properties.add (StringProperty ('urlPath' , urlPath));
628+ properties.add (FlagProperty ('hasAnimatedVersion' , value: hasAnimatedVersion,
629+ ifTrue: 'animatable' ,
630+ ifFalse: 'not animatable' ));
631+ }
632+ }
633+
600634class InlineVideoNode extends BlockContentNode {
601635 const InlineVideoNode ({
602636 super .debugHtmlNode,
@@ -1399,7 +1433,7 @@ class _ZulipContentParser {
13991433 if (imgElement.className == 'image-loading-placeholder' ) {
14001434 return ImagePreviewNode (
14011435 srcUrl: href,
1402- thumbnailUrl : null ,
1436+ thumbnail : null ,
14031437 loading: true ,
14041438 originalWidth: null ,
14051439 originalHeight: null ,
@@ -1411,19 +1445,21 @@ class _ZulipContentParser {
14111445 }
14121446
14131447 final String srcUrl;
1414- final String ? thumbnailUrl ;
1415- if (src.startsWith ('/user_uploads/thumbnail/' )) {
1448+ final ImageThumbnailLocator ? thumbnail ;
1449+ if (src.startsWith (ImageThumbnailLocator .urlPathPrefix )) {
14161450 // For why we recognize this as the thumbnail form, see discussion:
14171451 // https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279872
14181452 srcUrl = href;
1419- thumbnailUrl = src;
1453+ thumbnail = ImageThumbnailLocator (
1454+ urlPath: src,
1455+ hasAnimatedVersion: imgElement.attributes['data-animated' ] == 'true' );
14201456 } else {
14211457 // Known cases this handles:
14221458 // - `src` starts with CAMO_URI, a server variable (e.g. on Zulip Cloud
14231459 // it's "https://uploads.zulipusercontent.net/" in 2025-10).
14241460 // - `src` matches `href`, e.g. from pre-thumbnailing servers.
14251461 srcUrl = src;
1426- thumbnailUrl = null ;
1462+ thumbnail = null ;
14271463 }
14281464
14291465 double ? originalWidth, originalHeight;
@@ -1447,7 +1483,7 @@ class _ZulipContentParser {
14471483
14481484 return ImagePreviewNode (
14491485 srcUrl: srcUrl,
1450- thumbnailUrl : thumbnailUrl ,
1486+ thumbnail : thumbnail ,
14511487 loading: false ,
14521488 originalWidth: originalWidth,
14531489 originalHeight: originalHeight,
0 commit comments