Write inline-image widget code more like image-preview widget code#2102
Write inline-image widget code more like image-preview widget code#2102gnprice merged 14 commits intozulip:mainfrom
Conversation
gnprice
left a comment
There was a problem hiding this comment.
Thanks! A couple of questions below.
I think probably it'd make sense to factor out the helper widget first, and only then merge the branch. That way it gives us some confidence that the direction this is taking is the right one for our eventual destination — i.e. that we want to be mostly making InlineImage more like MessageImagePreview rather than vice versa.
lib/widgets/content.dart
Outdated
| thumbnailUrl: (node.src is ImageNodeSrcThumbnail && !node.loading) | ||
| ? resolvedSrc | ||
| : null, |
There was a problem hiding this comment.
Why is null preferable here in the non-thumbnail case? (The status quo being that we use resolvedSrc in all cases.)
The commit says:
content: Anticipate external images in how we choose lightbox thumbnailUrl
As of now (2026-01), servers don't put a non-thumbnail URL as `src` in
inline images, except for the loading case when it's the URL of a
spinner image. So this is just something that may be helpful for
future servers. (This widget code has an early return on
`node.loading`, so we don't need to include that as part of this
condition.)
but I don't see why that means we prefer null.
There was a problem hiding this comment.
Ah OK, yes, my reasoning was incomplete:
- If this code is reached, it means we know what URL to request for the lightbox display. The lightbox takes an optional
thumbnailUrlso it can show a low-res version of the image while that display image is being fetched. - When
node.loadingis true, we shouldn't useresolvedSrcforthumbnailUrlbecauseresolvedSrcis the "spinner" image URL, and we never want to show that spinner image because it's not consistent with our UI. We don't have a thumbnail of the actual image—that's the "loading" thatnode.loadingrepresents—so we can't tell the lightbox how to show a low-res version of the image while the original is being fetched. - When
srcis a thumbnail andnode.loadingis false, that's the common case, where we can give the lightbox what it wants (a low-res version of the image to show while the original is being fetched). That's unchanged in this commit. - When
srcis not a thumbnail andnode.loadingis false, that's not something we expect current servers to produce (for inline images). To decide whether to passresolvedSrcasthumbnailUrl, I notice that in this case we chooselightboxDisplayUrlto be identical toresolvedSrc. Therefore we know thatresolvedSrcisn't a low-res version of what we'll show in the lightbox, so it can't help the lightbox in the way that it wants.In fact, on first opening the lightbox (when the lightbox image hasn't been loaded and cached), passing(No, I guess actually it would've been loaded and cached, for the message-list display.) This is the actual, desired behavior in image previews, which I think it makes sense to match for inline images. In image previews, we understand that external images produce this case, which is why I framed it that way in the commit message.nullforthumbnailUrlshould prevent an unwanted double-request for the same image.
If desired, we could give the lightbox different behavior when we don't have a low-res image URL to pass—e.g. it could show a CupertinoActivityIndicator() while the original image is being fetched—but I think that's out of scope here.
lib/widgets/content.dart
Outdated
| context: context, | ||
| message: InheritedMessage.of(context), | ||
| messageImageContext: context, | ||
| src: lightboxDisplayUrl, |
There was a problem hiding this comment.
And here, it looks like the effect is to switch in the non-thumbnail case from resolvedOriginalSrc to resolvedSrc.
Reading the commit message a second time, I guess the point is that resolvedOriginalSrc would be null in that case?
content: Anticipate external image URLs in how we choose lightbox src
As of now (2026-01), servers don't put a non-thumbnail URL as `src` in
inline images, except for the loading case (which isn't relevant in
the touched code because of an early return on `node.loading` above
it).
So this is just meant to be helpful for potential future servers that
start producing inline-image HTML for external images, if they handle
the external-image case in much the same way as they already do in
image-preview HTML.
There was a problem hiding this comment.
I guess the point is that resolvedOriginalSrc would be null in that case?
It might be null—we can work that out with the future servers—but I think the point here is that we avoid requesting a URL like https://upload.wikimedia.org/etc/etc except when saving an image to the device, and we use a URL like /external_content/etc/etc/ for both the message content and the lightbox display. We don't know if the actual external URL would be carried by resolvedOriginalSrc, but it plausibly could be, so might as well avoid resolvedOriginalSrc for the lightbox display, as we do in image previews. I'm thinking of Alex's message about image previews, at #api documentation > documenting inline images @ 💬:
There are three things one might do with the resource -- view it in the message feed, view it by itself, and store it on the device. The default for those is
(src, src, href)and for modern thumbnails, it's(src, href, href)
…x display As of now (2026-01), servers don't put a non-thumbnail URL as `src` in inline images, except for the loading case (which isn't relevant in the touched code because of an early return on `node.loading` above it). If future servers do, the likely reason is that the image is an external (and not thumbnailed) image. The status quo for those in image previews is to reserve `node.originalSrc` (e.g. `https://upload.wikimedia.org/etc/etc`) for saving the image to the device, and to use `node.src` (e.g. `/external_content/etc/etc/`) for both the message content and the lightbox display. Discussion: https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279483 That choice seems reasonable to apply to this case in the inline-image feature, again just in case the case appears in future servers.
…equal src I don't expect bad behavior to result from passing the same URL for `src` and `thumbnailUrl`, assuming the logic supporting Image.network handles image-request caching well. But it looks odd, and the new `is ImageNodeSrcThumbnail` condition should give confidence that we're actually passing the kind of thing the lightbox expects for a param whose name is `thumbnailUrl`.
…review For why this works, see the code comment. Maybe odd that I didn't discover this while working with this code recently -- perhaps it might have simplified the refactors in zulip#2077, I'm not sure.
…lder This changes what we pass in one caller of the MessageMediaContainer constructor: the one with `onTap: null`, in the early return on `lightboxDisplayUrl == null`. That's fine and NFC; it means putting a `SizedBox.shrink()` inside the media container's SizedBox with dimensions 150x100. We should still design and use something other than SizedBox.shrink(), but we're tracking that with the `TODO(zulip#265)` in the code.
And, while doing so, write the code more parallel to the image-preview code too.
d85daef to
af57e4e
Compare
|
Thanks for the review!
That makes sense. Revision pushed, this time with that helper widget. |
gnprice
left a comment
There was a problem hiding this comment.
Thanks! Looks great, and those revised commit messages are helpful. Two nits.
lib/widgets/content.dart
Outdated
| @override | ||
| Widget build(BuildContext context) { | ||
| return _Image(node: node, size: MessageMediaContainer.size, | ||
| buildContainer: ({required onTap, required child}) { |
There was a problem hiding this comment.
nit: conventionally (in Flutter upstream), these buildFoo callbacks take positional arguments:
| buildContainer: ({required onTap, required child}) { | |
| buildContainer: (onTap, child) { |
lib/widgets/content.dart
Outdated
| typedef _ImageContainerBuilder = Widget Function({ | ||
| required VoidCallback? onTap, | ||
| required Widget child, | ||
| }); | ||
|
|
||
| /// A helper widget to deduplicate much of the logic in common | ||
| /// between image previews and inline images. | ||
| class _Image extends StatelessWidget { |
There was a problem hiding this comment.
nit: in this file, the entry point is at the top and callees below; so put this either after InlineImage below, or at the end of all the widgets before _launchUrl
Then if image previews gain an alt attribute, it'll naturally be picked up to create a Tooltip, just like in the inline-image case.
af57e4e to
86c6888
Compare
|
Thanks for the review! Revision pushed. |
|
Thanks! Looks good; merging. |
Greg suggested in #2067 (comment) :
This PR is meant to resolve the first paragraph, with the helper-widget suggestion in the second paragraph left for a future PR.
There are two behavior changes that are just about future-proofing in case servers start producing the inline-image HTML form for external images too:
9748f1a content: Anticipate external images in how we choose lightbox thumbnailUrl
684834a content: Anticipate external image URLs in how we choose lightbox src
And one behavior change that affects behavior with current servers:
d85daef content: In inline images, also offer the lightbox when
node.loading