-
Notifications
You must be signed in to change notification settings - Fork 306
content: Handle link previews #1049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
abdec10
to
8fba3fe
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Looks like this needs a rebase, so I'll do a more thorough review after that. But here are some comments from a quick skim (in particular I haven't read the parsing code or checked the UI code against web).
lib/widgets/content.dart
Outdated
padding: const EdgeInsets.symmetric(horizontal: 5), | ||
child: InsetShadowBox( | ||
bottom: 8, | ||
color: messageListTheme.streamMessageBgDefault, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it'll be wrong for DMs, and (in future) for messages where we highlight the background because of @-mentions in the message (#647).
lib/widgets/content.dart
Outdated
child: Text(node.title!, | ||
style: TextStyle( | ||
fontSize: 1.2 * kBaseFontSize, | ||
color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see a hard-coded color; does it follow web? It needs either a variable in ContentTheme
or this comment:
// Web has the same color in light and dark mode.
(Same for any other hard-coded colors.)
Please also post screenshots in light mode; I see screenshots for dark mode already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the screenshots.
lib/widgets/content.dart
Outdated
if (isSmallWidth) { | ||
return Container( | ||
decoration: const BoxDecoration(border: | ||
Border(left: BorderSide(color: Color(0xFFEDEDED), width: 3))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about hard-coded colors; also, I think we more often use lowercase (so 0xffededed
instead of 0xFFEDEDED
); here and below.
lib/widgets/content.dart
Outdated
final messageListTheme = MessageListTheme.of(context); | ||
final isSmallWidth = MediaQuery.sizeOf(context).width <= 576; | ||
|
||
final dataContainer = Container( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is dataContainer
the best name? I see the Container
widget being used…what's the "data" and how does that widget "contain" it?
How about building this method's return value with help from a mutable Widget result
variable? So here:
Widget result = Container(/* etc. */);
then below,
result = isSmallWidth
? Column(/* etc. */, children: [/* etc. */, result])
: Row(/* etc. */, children: [/* etc. */, result]);
then result = Container(decoration: /* etc. */, child: result);
and finally return result;
.
533fa33
to
aeabbe6
Compare
Thanks for the initial comments @chrisbobbe! Pushed a new revision, PTAL. |
What's a good way for me to test this; do I need to set up a dev server? 🙂 I see link previews are disabled on CZO; I've asked on CZO if there's a reason for that: https://chat.zulip.org/#narrow/channel/9-issues/topic/Link.20previews.20for.20Zulip.20URLs/near/2013846 |
Dev server, or make a test realm on Zulip Cloud. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below.
lib/widgets/content.dart
Outdated
GestureDetector( | ||
onTap: () => _launchUrl(context, node.hrefUrl), | ||
child: RealmContentNetworkImage( | ||
Uri.parse(node.imageSrcUrl), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about just one codepath for URL-parsing this string, instead of splitting by isSmallWidth
?
Also, this will throw an error if parsing fails. Instead of doing that, let's use Uri.tryParse
instead, similar to what we do in MessageImage
.
lib/model/content.dart
Outdated
if (second.nodes.length > 2) return null; | ||
|
||
String? title, description; | ||
for (final node in second.nodes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is both a title and a description, can they appear in either order? If not—if it's always the title first—how about requiring that? For the code structure, instead of a loop, maybe we could do a switch
on the length of second.nodes
, and in the 1
case we expect either a title or a description, and in the 2
case we expect a title first and then a description.
lib/model/content.dart
Outdated
final first = divElement.nodes.first; | ||
if (first is! dom.Element) return null; | ||
if (first.localName != 'a') return null; | ||
if (first.className != 'message_embed_image') return null; | ||
if (first.nodes.isNotEmpty) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can be a bit more compact in some places by using Dart Patterns; try a regex search for if.*case
in this file.
@override | ||
Widget build(BuildContext context) { | ||
final messageListTheme = MessageListTheme.of(context); | ||
final isSmallWidth = MediaQuery.sizeOf(context).width <= 576; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
576 is from the web app, right? And some other explicit width and height values below: 500, 80, 115, etc.
We could comment on each one, saying they come from the web app. But actually, I could imagine future design work where we want to tune these numbers to be different from the web app. In that case such comments would become wrong/misleading if we forgot to update them. So maybe best not.
To memoize the fact that they match web, though (so a reader doesn't have to check each one), let's mention it in the commit message.
lib/widgets/content.dart
Outdated
|
||
return Container( | ||
decoration: const BoxDecoration( | ||
border: Border(left: BorderSide( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BorderDirectional(start:
, right?
lib/widgets/content.dart
Outdated
// TODO(#647) use different color for highlighted messages | ||
// TODO(#681) use different color for DM messages | ||
color: messageListTheme.streamMessageBgDefault, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right; yeah, I forgot we haven't done #681 yet.
I guess this needs one more TODO I hadn't thought of before:
// TODO(#488) use different color for non-message contexts
Probably the desired effect of that TODO will be to guide the implementation toward a color param rather than a param that's about the aspects of a Zulip message.
lib/widgets/content.dart
Outdated
? titleAndDescription | ||
: LayoutBuilder( | ||
builder: (context, constraints) => ConstrainedBox( | ||
constraints: BoxConstraints( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot about constraints in this code: a Container.constraints
, an UnconstrainedBox
, a LayoutBuilder
, a ConstrainedBox.constraints
. I'm not really following it yet; do you think there might be a simpler way to write it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this LayoutBuilder
here.
lib/widgets/content.dart
Outdated
fit: BoxFit.cover, | ||
width: 80, | ||
height: 80, | ||
alignment: Alignment.center)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't Alignment.center
the default; can we leave out this argument?
lib/widgets/content.dart
Outdated
: LayoutBuilder( | ||
builder: (context, constraints) => ConstrainedBox( | ||
constraints: BoxConstraints( | ||
maxWidth: constraints.maxWidth - 115), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder about dropping this maxWidth - 115
detail, for a few benefits:
- More of the text can show before it gets clipped
- Removes the need for
LayoutBuilder
which isn't great for- performance
- code complexity (e.g. my difficulty in a previous comment)
- We allow more horizontal space for other paragraph content without issues
Could leave a code comment saying we're not following web in this way.
lib/widgets/content.dart
Outdated
border: Border(left: BorderSide( | ||
// Web has the same color in light and dark mode. | ||
color: Color(0xffededed), width: 3))), | ||
padding: const EdgeInsets.all(5), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Web also puts 5px bottom margin, a.k.a. --markdown-interelement-space-px
, in addition to this. In zulip-flutter do we have something systematic for vertical spacing between block elements, or is each element responsible for adding its own space at the bottom and/or top?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a quick test, looks like the spacing between different widgets is not consistent (or non-existent in some cases). On Web --markdown-interelement-space-px
is calculated from line-height (which for me becomes 6.72px instead of 5px), so we'd need something similar for zulip-flutter too. However, it'll need a sweep across all the content widgets therefore making it out-of-scope for this PR.
Should 5px bottom margin as a quick fix, suffice for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should 5px bottom margin as a quick fix, suffice for now?
Sure, sounds good
6e37971
to
70ea927
Compare
Thanks for the review @chrisbobbe! Pushed an update, PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks! Just nits below, and I think #1049 (comment) is still open. Otherwise LGTM; marking for Greg's review.
lib/widgets/content.dart
Outdated
@@ -148,6 +151,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> { | |||
final Color colorTableCellBorder; | |||
final Color colorTableHeaderBackground; | |||
final Color colorThematicBreak; | |||
final Color colorLink; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: keep in alphabetical order (here and elsewhere in class definition)
lib/widgets/content.dart
Outdated
@@ -1030,7 +1037,7 @@ class _InlineContentBuilder { | |||
_pushRecognizer(recognizer); | |||
final result = _buildNodes(node.nodes, | |||
// Web has the same color in light and dark mode. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove this comment; it would be easy for it to become wrong if the light/dark variants start to be different. Also you've already left a comment with the same meaning on the dark variant.
49cf612
to
aa4c9ea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @rajveermalviya for building this, and @chrisbobbe for the previous reviews!
Here's an initial round, just on model/content.dart
. Posting this now because about to switch to another task.
lib/model/content.dart
Outdated
// Ref: | ||
// https://ogp.me/ | ||
// https://oembed.com/ | ||
class LinkPreviewNode extends BlockContentNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's put this above TableNode and friends; it feels more analogous to embedding images and video
lib/model/content.dart
Outdated
// Ref: | ||
// https://ogp.me/ | ||
// https://oembed.com/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also link to whatever bits of Zulip docs there are for this.
The most on-point item I find in the Help Center is this:
https://zulip.com/help/image-video-and-website-previews#configure-whether-website-previews-are-shown
which at least provides the keyword for what we call it in user-facing text: "website previews". (Which in turn is potentially useful for searching chat.zulip.org for discussion of the feature, or for asking about it.)
test/model/content_test.dart
Outdated
'<p><a href="https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html">https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html</a></p>\n' | ||
'<div class="message_embed">' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indentation
'<p><a href="https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html">https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html</a></p>\n' | |
'<div class="message_embed">' | |
'<p><a href="https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html">https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html</a></p>\n' | |
'<div class="message_embed">' |
The div
is a sibling of the p
, not a child, so the indentation should express that.
lib/model/content.dart
Outdated
case [dom.Element(localName: 'div') && final single]: | ||
switch (single.className) { | ||
case 'message_embed_title': | ||
title = parseTitle(single); | ||
if (title == null) return null; | ||
case 'message_embed_description': | ||
description = parseDescription(single); | ||
if (description == null) return null; | ||
} | ||
|
||
default: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this would accept the children being a single div
with some other unexpected class. I think that's unintended.
lib/model/content.dart
Outdated
return null; | ||
} | ||
|
||
return LinkPreviewNode( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this parser code has (modulo my comment just above) an invariant that it always has at least one of title
or description
non-null when it decides to construct a LinkPreviewNode.
Is that invariant something you believe holds? It seems like the sort of thing that'd be helpful for reasoning about (the avoidance of) edge cases when displaying these as widgets.
If so, we can add an assert
at the constructor to confirm that it holds. That'd make it something the widgets code can count on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, turns out it is possible for both title and description to be absent. For example, the description is already considered optional but if the website's HTML has neither the <title>…</title>
nor <meta property="og:title" … />
then the title will also be missing, resulting in server to generate an empty data-container
:
<div class="message_embed">
<a class="message_embed_image" … >…</a>
<div class="data-container"></div>
</div>
(If the <meta property="og:image" … />
is also missing then server doesn't generate "website preview" message for that link)
I'll update the implementation to support the empty data-container
.
lib/model/content.dart
Outdated
} else { | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sure am hoping for Dart to at some point gain an analogue of Rust's let-else, or Swift's guard-let 🙂
aa4c9ea
to
193280a
Compare
Thanks for the review @gnprice! Pushed an update, PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! The revisions look good; just two nits below.
Next I'll look at the remaining parts of the PR.
lib/model/content.dart
Outdated
case []: | ||
// Server generates an empty `<div class="data-container"></div>` | ||
// if website HTML doesn't have both title (derived from | ||
// `og:title` or `<title>…</title>`) and description (derived from | ||
// `og:description`). | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case []: | |
// Server generates an empty `<div class="data-container"></div>` | |
// if website HTML doesn't have both title (derived from | |
// `og:title` or `<title>…</title>`) and description (derived from | |
// `og:description`). | |
break; | |
case []: | |
// Server generates an empty `<div class="data-container"></div>` | |
// if website HTML has neither title (derived from | |
// `og:title` or `<title>…</title>`) nor description (derived from | |
// `og:description`). | |
break; |
I'd read "if doesn't have both A and B" as meaning "if not (has A and has B)". But I think that's not what you meant — if that were the case, the single-child case below shouldn't be possible.
lib/model/content.dart
Outdated
|
||
String? title, description; | ||
switch (dataContainer.nodes) { | ||
case []: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: either put this case after the other two, or swap the order of the other two — that way the cases of 0, 1, and 2 children go in consecutive order (in one direction or the other)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, and finished reading the rest of the PR — comments below.
lib/widgets/content.dart
Outdated
// But for now we use a static value instead, see discussion: | ||
// https://github.com/zulip/zulip-flutter/pull/1049#discussion_r1915747908 | ||
padding: const EdgeInsets.only(bottom: 5), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reasoning makes sense but let's use a value that corresponds to web's new defaults, rather than "dense mode" / web's old defaults.
That way it's most consistent with the rest of our design — our font size and line height are based on web's new defaults.
lib/model/content.dart
Outdated
@@ -1339,6 +1391,112 @@ class _ZulipContentParser { | |||
return EmbedVideoNode(hrefUrl: href, previewImageSrcUrl: imgSrc, debugHtmlNode: debugHtmlNode); | |||
} | |||
|
|||
static final _websitePreviewImageSrcRegexp = RegExp(r'background-image: url\("(.+)"\)'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I tried opening an ancient example message on CZO, intending to try out how the UI looks… and found that it didn't parse. 🙂.
I think the issue may only be that the argument of url
here is lacking quotes. If it's easy to make that old message work by handling that, then it'd be good to do so in this PR while we're thinking about it — we'll want to cover old messages like this eventually anyway.
(Probably cleanest to do so as a separate commit on top, letting the main commit stay focused on the modern case.)
lib/widgets/content.dart
Outdated
// TODO(#647) use different color for highlighted messages | ||
// TODO(#681) use different color for DM messages | ||
color: MessageListTheme.of(context).streamMessageBgDefault, | ||
child: UnconstrainedBox( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be OverflowBox
instead?
This widget's doc suggests that it's not meant for cases where the child will overflow:
/// In debug mode, if the child overflows the container, a warning will be
/// printed on the console, and black and yellow striped areas will appear where
/// the overflow occurs.
whereas that other widget is.
(I haven't read enough to fully understand what the difference is, though.)
lib/widgets/content.dart
Outdated
child: Text(node.title!, | ||
style: TextStyle( | ||
fontSize: 1.2 * kBaseFontSize, | ||
height: kTextHeightNone, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh curious. Why this "none" value instead of a specific line-height?
This is one bit that looks different between web and this PR in the screenshots — the titles have a bigger line-height on web.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Web uses line-height: normal
for this. MDN docs for it says that it can be roughly 1.2
. Which seems correct, atleast on Firefox desktop and Firefox mobile for Android:
firefox-fonts-tool.mp4
So, updated to use that value.
lib/widgets/content.dart
Outdated
child: RealmContentNetworkImage( | ||
resolvedImageSrcUrl, | ||
fit: BoxFit.cover, | ||
width: 80, | ||
height: 80)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell (I just spent a few minutes digging through the implementation of Image
to investigate this), the width
and height
parameters here don't do anything beyond what wrapping this in a Size
widget would do.
It looks like all our other RealmContentNetworkImage
widgets take the latter approach — skipping these parameters, and relying on their parents for size — and that seems to work fine.
So let's do that. Then the rest of this widget's details, and its GestureDetector parent, can be deduplicated with the small-width case above.
Widget build(BuildContext context) { | ||
final store = PerAccountStoreWidget.of(context); | ||
final resolvedImageSrcUrl = store.tryResolveUrl(node.imageSrcUrl); | ||
final isSmallWidth = MediaQuery.sizeOf(context).width <= 576; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love using MediaQuery.sizeOf here — it feels like it's inevitably asking the wrong question. (Instead of the width of the entire app's viewport, it'd be much more to the point to specify this design in terms of this widget's own width, which will have been dictated by its parent.)
But this is fine, because I think implementing this design, with the way it flips between wide and tall forms, in that cleaner way would require either (a) LayoutBuilder, which is significantly less clean in other ways, or (b) significantly more work. And the exact design of this feature isn't something I'd want us to spend a lot of time on.
test/model/content_test.dart
Outdated
description: null), | ||
]); | ||
|
||
static const websitePreviewWithoutTitleAndDescription = ContentExample( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static const websitePreviewWithoutTitleAndDescription = ContentExample( | |
static const websitePreviewWithoutTitleOrDescription = ContentExample( |
await prepare(tester, ContentExample.websitePreviewSmoke.html); | ||
tester.widget(find.byType(WebsitePreview)); | ||
|
||
await tester.tap(find.text('Zulip — organized team chat')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more quick check we can add to the two of these that have a description: check find.text
finds something with the description's text.
(Otherwise e.g. this test case and the websitePreviewWithoutDescription
one below are making exactly the same set of checks, I think, which seems incomplete since there are expected differences in behavior.)
test/widgets/content_test.dart
Outdated
tester.widget(find.byType(WebsitePreview)); | ||
|
||
await tester.tap(find.byType(RealmContentNetworkImage)); | ||
check(testBinding.takeLaunchUrlCalls()) | ||
.single.equals((url: url, mode: LaunchMode.platformDefault)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The find.byType(WebsitePreview)
check can be left out of these, I think — we're checking that there's an image which if you tap it opens the example's URL, and that seems like good confirmation in itself that we didn't just not manage to render the UI for this feature.
The advantage of leaving it out is that it's one fewer thing that has to be changed if we make a refactor to this code (for example even just renaming the widget class), one that isn't meant to affect the UI the user sees.
52af930
to
b5480d4
Compare
Thanks for the review @gnprice! Pushed an update, PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Generally this all looks good — a few small comments.
lib/model/content.dart
Outdated
@@ -1339,6 +1391,112 @@ class _ZulipContentParser { | |||
return EmbedVideoNode(hrefUrl: href, previewImageSrcUrl: imgSrc, debugHtmlNode: debugHtmlNode); | |||
} | |||
|
|||
static final _websitePreviewImageSrcRegexp = RegExp(r'background-image: url\("?(.+?)"?\)'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bothers me a bit because it would match if for some reason there was a "
on one side but not the other.
We can avoid that with a backreference in the regexp:
static final _websitePreviewImageSrcRegexp = RegExp(r'background-image: url\("?(.+?)"?\)'); | |
static final _websitePreviewImageSrcRegexp = RegExp(r'background-image: url\(("?)(.+?)\1\)'); |
(and adjusting .group(1)
below to .group(2)
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(No need for a test for that case, though — it seems awfully unlikely in practice.)
return null; | ||
} | ||
|
||
return WebsitePreviewNode( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to pass debugHtmlNode
— I noticed that while doing some debugging to follow up on #1049 (comment) 🙂
(The issue turned out to be that I'd done only half of the edits I suggested in the backreference comment above. Should make sure we support debugging, though.)
lib/widgets/content.dart
Outdated
]) | ||
: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ | ||
if (image != null) | ||
AspectRatio(aspectRatio: 1, child: image), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about an 80x80 SizedBox instead? That's simpler to think about, and I think it ends up doing the same thing here.
(If it didn't do the same thing, that would probably be an issue with the AspectRatio version — I think we want the fixed size.)
c2c5829
to
30b70ea
Compare
Thanks for the review @gnprice! Pushed an update, PTAL. |
Implements support for displaying website previews messages, follows the Web styling, like having different layout for larger viewports (> 576), and any other constraints that are empirically present on Web. Fixes: zulip#1016
In legacy website preview messages, the image URL in `message_embed_image` element's `style` is formatted as: background-image: url(https://example.com/image.png) In the latest server revision, it is formatted as: background-image: url("https://example.com/image.png") So, fix the regexp to match the URL whether or not it's enclosed in quotes.
30b70ea
to
e0df0ed
Compare
Thanks! Looks good; merging. |
Fixes: #1016
Screenshots