content: Record href and src straightforwardly on ImagePreviewNode#2077
content: Record href and src straightforwardly on ImagePreviewNode#2077gnprice merged 21 commits intozulip:mainfrom
Conversation
|
Prompted by #2067 (comment). |
gnprice
left a comment
There was a problem hiding this comment.
Thanks for this refactor! Seems helpful.
In retrospect when we changed the meaning of srcUrl (in 7db3044, #820) it would have been good to find a clearer name for its new meaning. I agree that this name really sounds like it means the value of the src attribute, given that this is in the context of something that parsed an HTML img element.
/cc @rajveermalviya: it'll be helpful for you to follow these changes, since you've been involved in this part of the code.
test/model/content_test.dart
Outdated
| // and the message in the message event was already in the non-loading form.) | ||
| null, | ||
| '<div class="message_inline_image">' | ||
| '<a href="/user_uploads/path/to/example.png" title="example.jpg">' |
There was a problem hiding this comment.
nit:
| '<a href="/user_uploads/path/to/example.png" title="example.jpg">' | |
| '<a href="/user_uploads/path/to/example.png" title="example.png">' |
(matches example in docs, and seems logical)
test/widgets/content_test.dart
Outdated
| doTest('thumbnail', | ||
| rawHref: '/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg', | ||
| rawSrc: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp', | ||
| expectUrlInPreview: eg.realmUrl.resolve('/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'), | ||
| expectLoadingIndicator: false, | ||
| expectUrlInLightbox: eg.realmUrl.resolve('/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg'), | ||
| expectThumbnailUrlInLightbox: eg.realmUrl.resolve('/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'), | ||
| example: ContentExample.imagePreviewSingle); |
There was a problem hiding this comment.
Recording here a remark I made on our call today: I think for reading these tests it would help to pull out these URLs as local variables. That way it's easier to spot what the pattern is of which of the different expectations are meant to match which of the two URLs at the top. So for example:
| doTest('thumbnail', | |
| rawHref: '/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg', | |
| rawSrc: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp', | |
| expectUrlInPreview: eg.realmUrl.resolve('/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'), | |
| expectLoadingIndicator: false, | |
| expectUrlInLightbox: eg.realmUrl.resolve('/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg'), | |
| expectThumbnailUrlInLightbox: eg.realmUrl.resolve('/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'), | |
| example: ContentExample.imagePreviewSingle); | |
| final rawHref = '/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg'; | |
| final rawSrc = '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'; | |
| doTest('thumbnail', | |
| rawHref: rawHref, | |
| rawSrc: rawSrc, | |
| expectLoadingIndicator: false, | |
| expectUrlInPreview: eg.realmUrl.resolve(rawSrc), | |
| expectUrlInLightbox: eg.realmUrl.resolve(rawHref), | |
| expectThumbnailUrlInLightbox: eg.realmUrl.resolve(rawSrc), | |
| example: ContentExample.imagePreviewSingle); |
Then further changes:
- Can make the helper something that's called from the
testWidgetsbody, rather than callingtestWidgetsitself; that makes a handy scope per test for defining such variables. - Can tweak the API so that the helper takes care of calling
eg.realmUrl.resolve. - Moving
expectLoadingIndicatorout from in between the URLs might help a bit further in seeing the pattern of the URLs.
There was a problem hiding this comment.
Oh and one that just came to mind, not from the call:
- The thumbnail in lightbox logically comes before the main URL there, right? It's what's used while the other is still loading. So could reorder that earlier.
There was a problem hiding this comment.
Can tweak the API so that the helper takes care of calling
eg.realmUrl.resolve.
I think I'd prefer if the helper could take the expectFooUrl params literally, so not a string but a Uri, and not a variable with "raw" in it. But the eg.realmUrl.resolve calls do make for a lot of repeated logic. How about a separate helper that deduplicates those calls, like this:
Future<void> doTest() {} // etc.
Uri url(String reference) => eg.realmUrl.resolve(reference);
testWidgets('thumbnail', (tester) async {
final rawHref = '/user_uploads/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg';
final rawSrc = '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp';
await doTest(tester,
rawHref: rawHref,
rawSrc: rawSrc,
expectLoadingIndicator: false,
expectUrlInPreview: url(rawSrc),
expectThumbnailUrlInLightbox: url('/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'),
expectUrlInLightbox: url(rawHref),
example: ContentExample.imagePreviewSingle);
});
test/widgets/content_test.dart
Outdated
| if (expectUrlInLightbox != null) { | ||
| check(lightboxPage).isNotNull(); | ||
| check(lightboxPage!.src).equals(expectUrlInLightbox); | ||
| } else { | ||
| check(lightboxPage).isNull(); | ||
| } |
There was a problem hiding this comment.
And recording a nit/suggestion I made on our call: I believe this is equivalent:
| if (expectUrlInLightbox != null) { | |
| check(lightboxPage).isNotNull(); | |
| check(lightboxPage!.src).equals(expectUrlInLightbox); | |
| } else { | |
| check(lightboxPage).isNull(); | |
| } | |
| check(lightboxPage?.src).equals(expectUrlInLightbox); |
| required String rawSrc, | ||
| required Uri? expectUrlInPreview, | ||
| required bool expectLoadingIndicator, | ||
| required Uri? expectThumbnailUrlInLightbox, |
There was a problem hiding this comment.
also from our call: it looks like this parameter doesn't get used
lib/model/content.dart
Outdated
| /// when the original image is in an uncommon format, like TIFF. | ||
| /// This isn't implemented yet; it's #1268. | ||
| // TODO(#1268) implement transcoded-image feature; update dartdoc | ||
| final String href; |
There was a problem hiding this comment.
Another item from our call: it sounds like when extending this to the new inline images you'll want to call this more like originalSrc; in that case it's probably clearest to call it that within this PR. That's quite closely aligned with what the dartdoc describes this as (the "canonical source URL" vs the "original source"), and it's also closer to the status quo ante than href is.
| onTap: resolvedSrcUrl == null ? null : () { // TODO(log) | ||
| onTap: () { |
There was a problem hiding this comment.
Can some of these rearrangements of logic get pulled into an NFC prep commit? I think that'd help with reading the substantive changes in this file.
I'm thinking of:
- this conditional moving to an early return above
- this variable being renamed
- the
node.loadingconditional being moved upward
and there are likely another item or two like those. They're all small and probably fit neatly all together into one commit, but it'd be helpful to separate that from the behavior changes.
| ImagePreviewNode( | ||
| srcUrl: '/external_content/de28eb3abf4b7786de4545023dc42d434a2ea0c2/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067', | ||
| thumbnail: null, | ||
| href: 'https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg', | ||
| src: ImagePreviewNodeSrcOther('/external_content/de28eb3abf4b7786de4545023dc42d434a2ea0c2/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067'), |
There was a problem hiding this comment.
Hmm, interesting that this exposes some URLs we'd been completely ignoring before (the href here).
4d63302 to
f7726af
Compare
|
Thanks! Revision pushed, with quite a few more NFC commits, and the behavior change we discussed isolated in its own commit, plus another I noticed when I fixed a bug in the tests 🙂. The two behavior changes are (excluding ones caused by bad server behavior, which also have their own commits):
|
79276d1 to
1f28169
Compare
|
Oops, noted a few fixes after pushing, and pushed again. Dinner time, so I'll leave this here for now. |
|
@rajveermalviya would you give this a review? Greg is out-of-office for several days and won't see this until he gets back. 🙂 |
rajveermalviya
left a comment
There was a problem hiding this comment.
This is a very clean refactor, thanks @chrisbobbe! Reviewed through all commits and it all looks good to me.
|
Thanks for the review! |
| message: message, | ||
| messageImageContext: context, | ||
| src: resolvedSrcUrl, | ||
| thumbnailUrl: resolvedThumbnailUrl, |
There was a problem hiding this comment.
I'm looking at this next-to-last commit:
4497ea1 content [nfc]: In image-preview widget, make variables for src and href
to try to confirm it's NFC, and I'm not sure it is.
There's a lot there, so I started by breaking it down into cases: first, what if node.loading is true? (Because that's the first case in the if/else chain this commit introduces.)
In that case, the old code would set thumbnailUrl to node.thumbnail?.resolve( etc. The new code looks like it will set it always to null. So whenever node.thumbnail is non-null and node.loading is true, then it looks like the behavior will differ.
There was a problem hiding this comment.
Ah yeah, that makes sense. (As we discussed in the office, I think this particular case isn't a behavior change in this commit because the parser doesn't produce a node whose thumbnail is non-null and loading is true, but this separate commit doesn't increase the branch's readability, so for the next revision I've squashed it with the main commit that follows it.)
lib/model/content.dart
Outdated
| /// This is expected to be true (in 2026-01) | ||
| /// while an uploaded image is being thumbnailed. |
There was a problem hiding this comment.
nit:
| /// This is expected to be true (in 2026-01) | |
| /// while an uploaded image is being thumbnailed. | |
| /// This is expected to be true (as of 2026-01) | |
| /// while an uploaded image is being thumbnailed. |
The difference is subtle, but to me "as of 2026-01" suggests (correctly) that we expect the same situation will probably continue for some time, even if we don't know when it might change; whereas "in 2026-01" suggests that we expect the situation is specific to this month.
lib/model/content.dart
Outdated
| /// | ||
| /// Clients are expected to use this URL when saving the image to the device. | ||
| /// | ||
| /// For images processed in modern thumbnailing (2026-01), |
There was a problem hiding this comment.
nit: something like
| /// For images processed in modern thumbnailing (2026-01), | |
| /// For images processed in modern thumbnailing (as of 2026-01), |
As is, it sounds like it's saying "modern thumbnailing" itself dates to 2026-01. In fact it's been stable for some time; I believe it dates to Zulip Server 9.0, in 2024.
We don't intend to *use* the `src` in the loading case; it's documented as being a hard-coded "spinner" image URL: https://zulip.com/api/message-formatting#image-loading-placeholders But moving this stanza downward is helpful on the way to unifying the image-preview parsing code with new code for the "inline image" feature, coming up.
This makes a few tests redundant; we'll remove them in a later commit, to not distract from the story of these tests. These tests will help verify an upcoming refactor, where we align ImagePreviewNode more closely with the HTML it's meant to represent.
This refactor helps toward unifying the image-preview parsing code
with new code for the "inline image" feature, coming up.
The `originalWidth` and `originalHeight` fields may now sometimes be
present in the loading case. The widget code doesn't read them --
MessageMediaContainer has fixed dimensions, 100px by 150px -- but
it's harmless to let them be there.
This commit is NFC with servers with expected behavior, but will
start rejecting content in the loading case if
`data-original-dimensions` is present and malformed (i.e. not in the
"{width}x{height}" format).
I've made a note of this on zulip#297 "Set mouse cursor style for links in message content" so we eventually consider it: zulip#297 (comment) It's not something we'll get around to in the short term, though, so I think the TODO is more distracting than it's worth.
The instance will always have a format we can fall back to, i.e., defaultFormatSrc, so there's no need for this method's return type to be nullable.
First, the thing that's meant to be wrapped in a LightboxHero (when we have data for that). Then the LightboxHero, unless we got nothing from the first step, which happens when the URL for the image preview was invalid (and we have a TODO for that).
This highlights the desired behavior in the image preview of not ever falling back to a thumbnailed image's original image.
…heck And repeat its implementation in the place we compute the URL for the image preview.
I've frequently gotten confused about the roles of `srcUrl` and `thumbnail`. The former has been especially confusing while thinking through all the cases we have to handle. The name `srcUrl` seemed to suggest (a) parsing something into a Uri object and (b) representing the `src` attribute in the HTML. Neither was true: it was a String, and it held the HTML's `href` at least as commonly as `src`. To straighten this out, just have ImagePreviewNode hold `href` and `src`, and transcribe the fields' meanings from the API doc and CZO discussions. The `src` field is a new type ImagePreviewNodeSrc with two subclasses corresponding to the thumbnail- and not-thumbnail-URL cases.
1f28169 to
df60c98
Compare
|
Thanks for the review! Revision pushed. |
|
That's helpful, thanks! I was able to verify that the new squashed last commit is NFC, after working through its logic for three cases:
Looks good; merging. |
|
Thanks! |
…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.
…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.
Notable commit message: