Skip to content

Write inline-image widget code more like image-preview widget code#2102

Merged
gnprice merged 14 commits intozulip:mainfrom
chrisbobbe:pr-inline-image-widget-code-refactor
Jan 29, 2026
Merged

Write inline-image widget code more like image-preview widget code#2102
gnprice merged 14 commits intozulip:mainfrom
chrisbobbe:pr-inline-image-widget-code-refactor

Conversation

@chrisbobbe
Copy link
Collaborator

Greg suggested in #2067 (comment) :

Perhaps as a followup PR, so while this is still fresh in mind but without blocking us merging and releasing the feature: this InlineImage is doing very nearly the same things as the MessageImagePreview widget, especially here in the _buildContent method, but the code looks more different than it is. It'd be nice to refactor them to look more similar so that the real differences are easier to spot.

Ideally, the logic here in _buildContent would become a helper widget that's shared between InlineImage and MessageImagePreview.

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

@chrisbobbe chrisbobbe requested a review from gnprice January 27, 2026 02:31
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label Jan 27, 2026
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! 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.

Comment on lines +1378 to +1380
thumbnailUrl: (node.src is ImageNodeSrcThumbnail && !node.loading)
? resolvedSrc
: null,
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Jan 27, 2026

Choose a reason for hiding this comment

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

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 thumbnailUrl so it can show a low-res version of the image while that display image is being fetched.
  • When node.loading is true, we shouldn't use resolvedSrc for thumbnailUrl because resolvedSrc is 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" that node.loading represents—so we can't tell the lightbox how to show a low-res version of the image while the original is being fetched.
  • When src is a thumbnail and node.loading is 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 src is not a thumbnail and node.loading is false, that's not something we expect current servers to produce (for inline images). To decide whether to pass resolvedSrc as thumbnailUrl, I notice that in this case we choose lightboxDisplayUrl to be identical to resolvedSrc. Therefore we know that resolvedSrc isn'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 null for thumbnailUrl should prevent an unwanted double-request for the same image. (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.

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.

context: context,
message: InheritedMessage.of(context),
messageImageContext: context,
src: lightboxDisplayUrl,
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Jan 27, 2026

Choose a reason for hiding this comment

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

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.
@chrisbobbe chrisbobbe force-pushed the pr-inline-image-widget-code-refactor branch from d85daef to af57e4e Compare January 28, 2026 21:23
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review!

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.

That makes sense. Revision pushed, this time with that helper widget.

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! Looks great, and those revised commit messages are helpful. Two nits.

@override
Widget build(BuildContext context) {
return _Image(node: node, size: MessageMediaContainer.size,
buildContainer: ({required onTap, required child}) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: conventionally (in Flutter upstream), these buildFoo callbacks take positional arguments:

Suggested change
buildContainer: ({required onTap, required child}) {
buildContainer: (onTap, child) {

Comment on lines +629 to +636
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 {
Copy link
Member

Choose a reason for hiding this comment

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

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.
@chrisbobbe chrisbobbe force-pushed the pr-inline-image-widget-code-refactor branch from af57e4e to 86c6888 Compare January 29, 2026 04:59
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

@gnprice
Copy link
Member

gnprice commented Jan 29, 2026

Thanks! Looks good; merging.

@gnprice gnprice merged commit 86c6888 into zulip:main Jan 29, 2026
1 check passed
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.

2 participants