Skip to content

Commit e715d12

Browse files
committed
content: Parse data-animated on image-preview HTML
This might've been done more simply by just adding the boolean value as a new field like `thumbnailAnimated` on ImagePreviewNode. But this value works closely with some structured data in thumbnail URL paths, here in image previews and also in the upcoming "inline images" feature, #1913. So this new ImageThumbnailLocator class seemed like a helpful way to present the data to consumers.
1 parent 75f1553 commit e715d12

File tree

4 files changed

+122
-49
lines changed

4 files changed

+122
-49
lines changed

lib/model/content.dart

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
600634
class 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,

lib/widgets/content.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ class MessageImagePreview extends StatelessWidget {
637637

638638
// TODO image hover animation
639639
final srcUrl = node.srcUrl;
640-
final thumbnailUrl = node.thumbnailUrl;
640+
final thumbnailUrl = node.thumbnail?.urlPath;
641641
final store = PerAccountStoreWidget.of(context);
642642
final resolvedSrcUrl = store.tryResolveUrl(srcUrl);
643643
final resolvedThumbnailUrl = thumbnailUrl == null

0 commit comments

Comments
 (0)