Skip to content

Conversation

@chrisbobbe
Copy link
Collaborator

Fixes #1936.

@chrisbobbe chrisbobbe added the maintainer review PR ready for review by Zulip maintainers label Nov 13, 2025
Copy link
Member

@rajveermalviya rajveermalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @chrisbobbe! LGTM, just one small comment.

Comment on lines 29 to 31
--data-urlencode 'narrow=[{"operator":"sender", "operand":2187},
--data-urlencode 'narrow=[{"operator":"sender", "operand":13313},
{"operator":"stream", "operand":"test here"},
{"operator":"topic", "operand":"content"}]' \
{"operator":"topic", "operand":"Thumbnails"}]' \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left from debug?

@rajveermalviya rajveermalviya added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Nov 17, 2025
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking care of this! Comments below.

Comment on lines 557 to 559
/// Use [ImageThumbnailLocatorExtension.resolve] to obtain a suitable URL
/// for the current UI need.
/// It may not work without adding authentication credentials to the request.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like information for the ImageThumbnailLocator doc.

}

@override
int get hashCode => Object.hash(urlPath, hasAnimatedVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hashCode, seed the hash with the class name so that the hash doesn't collide with other objects that happen to also consist of a string and a bool:

Suggested change
int get hashCode => Object.hash(urlPath, hasAnimatedVersion);
int get hashCode => Object.hash('ImageThumbnailLocator', urlPath, hasAnimatedVersion);

Comment on lines 738 to 741
thumbnail: ImageThumbnailLocator(
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp',
hasAnimatedVersion: false,
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can keep these a bit more compact:

Suggested change
thumbnail: ImageThumbnailLocator(
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp',
hasAnimatedVersion: false,
),
thumbnail: ImageThumbnailLocator(hasAnimatedVersion: false,
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'),

Comment on lines 758 to 759
urlPath: '/user_uploads/thumbnail/2/9f/tZ9c5ZmsI_cSDZ6ZdJmW8pt4/2c8d985d.gif/840x560-anim.webp',
hasAnimatedVersion: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. From this URL path, it sounds like the given path is the animated version. That makes the name "has animated version" seem a bit misleading.

The parser shows the attribute name in the HTML is data-animated. The most direct translation of that would be to call this field just animated. How about we use that name?

Comment on lines -562 to +563
final String? thumbnailUrl;
final ImageThumbnailLocator? thumbnail;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be helpful to split this commit:

  • An NFC prep commit makes the change on this line, and the other changes that makes necessary, with the new class having only urlPath.
  • Then a separate commit adds hasAnimatedVersion (or animated per another comment).

There are a lot of places that change in boring ways just from moving this URL path out to the new class, so it'd make the substantive changes easier to read if they were in a separate commit from that.

Comment on lines 172 to 180
final store = PerAccountStoreWidget.of(context);
ThumbnailFormat? bestCandidate;

final animateIfSupported = animationMode.resolve(context);
if (hasAnimatedVersion && animateIfSupported) {
bestCandidate ??= _bestFormatOf(store.sortedAnimatedThumbnailFormats,
width: widthPhysicalPx, height: heightPhysicalPx);
}

bestCandidate ??= _bestFormatOf(store.sortedStillThumbnailFormats,
width: widthPhysicalPx, height: heightPhysicalPx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: join up the bestCandidate computation as one stanza (and separate it from getting store)

Comment on lines +184 to +187
if (bestCandidate == null) {
// Odd if we'd need to fall back to the format encoded in [locator]'s path.
// Seems theoretically possible though:
// maybe this format isn't used now, for new uploads,
// but it was used in the past, including for this image.
return store.realmUrl.replace(path: urlPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is normal if there are no thumbnail formats at all, right?

In fact I think if this happens, it must be the case that there are no still thumbnail formats.

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "there are no thumbnail formats at all", do you mean server_thumbnail_formats in the /register response was empty? I assume the common reason for that is that no uploaded images have ever been thumbnailed, and in that scenario this code won't be reached.

This condition is about what to do when server_thumbnail_formats was empty but somehow we have a thumbnail URL in our hands. A thumbnail URL encodes a thumbnail format at the end, like "/840x560.webp". I'm assuming this can happen if an image was thumbnailed but then later the server admin said "hmm actually we don't want to do thumbnailing for new uploads anymore". We can be optimistic that the thumbnail is still reachable and go ahead and request it.

Comment on lines +189 to +190
return store.realmUrl.replace(path: urlPath);
}

final lastSlashIndex = urlPath.lastIndexOf('/');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines both assume that the URL string in urlPath is indeed just a path, and doesn't also contain a query and/or fragment.

I think it'd be reasonable for the API to guarantee that, but it doesn't appear to now (at https://zulip.com/api/message-formatting#images ). Without such a guarantee, a query component (though maybe not a fragment) would be a reasonable choice for this bit of API in isolation — e.g. Gravatar does that, as seen in our GravatarUrl.

(OTOH we do know it starts with a path component, because it starts with /u.)

So it'd be good to discuss that in #api documentation or #api design. Also to have the ImageThumbnailLocator constructor assert it, like it already does for the start of the path. A suitable check would be that there is no ? or # character.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/1000x2000.png');
});

testWidgets('animated version does not exist', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably clearest to put this test before its sibling — this seems like the simpler case to think about, and covers most of the interesting logic (and is surely the most common case)

Then the other test can focus just on what changes when hasAnimatedVersion is true.

Comment on lines 148 to 151
doCheck(tester, 250, 450, false, hasAnimatedVersion: false,
'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/500x900.jpg');
doCheck(tester, 300, 250, true, hasAnimatedVersion: false,
'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/840x560.webp');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why do these get different results? Both formats should be big enough for both these desired sizes, right?

… Ah, I see: it's because they're in physical pixels.

Would be good to have a comment highlighting that. 🙂 That's also one of the key points this test is making, so it's good to make it explicit.

@chrisbobbe chrisbobbe force-pushed the pr-thumbnail-formats branch from 7d51241 to c7bb1d8 Compare December 6, 2025 02:16
@chrisbobbe
Copy link
Collaborator Author

Thanks! Revision pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

content: Disable image-preview animations, subject to device settings

3 participants