From 2cf8f19082b490df9a3b45834c94c03ccb1f2258 Mon Sep 17 00:00:00 2001
From: Shu Chen
Date: Thu, 25 Jan 2024 12:07:36 +0000
Subject: [PATCH 1/5] content [nfc]: Remove alignment that has no effect
---
lib/widgets/content.dart | 1 -
1 file changed, 1 deletion(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 365153d25b..ff257d8963 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -262,7 +262,6 @@ class MessageImage extends StatelessWidget {
child: Container(
height: 100,
width: 150,
- alignment: Alignment.center,
color: const Color.fromRGBO(0, 0, 0, 0.03),
child: LightboxHero(
message: message,
From b193ce0f5357741418ce95971de62af2c6af66af Mon Sep 17 00:00:00 2001
From: Shu Chen
Date: Thu, 25 Jan 2024 12:08:51 +0000
Subject: [PATCH 2/5] content [nfc]: Decompose Container in MessageImage
---
lib/widgets/content.dart | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index ff257d8963..4a29f9b7f8 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -259,16 +259,17 @@ class MessageImage extends StatelessWidget {
// The corresponding element on web has a 5px two-sided margin…
// and then a 1px transparent border all around.
padding: const EdgeInsets.fromLTRB(1, 1, 6, 6),
- child: Container(
- height: 100,
- width: 150,
+ child: ColoredBox(
color: const Color.fromRGBO(0, 0, 0, 0.03),
- child: LightboxHero(
- message: message,
- src: resolvedSrc,
- child: RealmContentNetworkImage(
- resolvedSrc,
- filterQuality: FilterQuality.medium))))));
+ child: SizedBox(
+ height: 100,
+ width: 150,
+ child: LightboxHero(
+ message: message,
+ src: resolvedSrc,
+ child: RealmContentNetworkImage(
+ resolvedSrc,
+ filterQuality: FilterQuality.medium)))))));
}
}
From da68a5147177ee04e237546288cb3ee1183f6e7b Mon Sep 17 00:00:00 2001
From: Shu Chen
Date: Thu, 25 Jan 2024 12:11:53 +0000
Subject: [PATCH 3/5] content: Tweak MessageImage size to match web
The transparent border mentioned in the removed comment
was treated as external padding to the image but it's
actually inner padding within the gray space.
---
lib/widgets/content.dart | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 4a29f9b7f8..db6dd5b6d6 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -256,20 +256,20 @@ class MessageImage extends StatelessWidget {
child: Padding(
// TODO clean up this padding by imitating web less precisely;
// in particular, avoid adding loose whitespace at end of message.
- // The corresponding element on web has a 5px two-sided margin…
- // and then a 1px transparent border all around.
- padding: const EdgeInsets.fromLTRB(1, 1, 6, 6),
+ padding: const EdgeInsets.only(right: 5, bottom: 5),
child: ColoredBox(
color: const Color.fromRGBO(0, 0, 0, 0.03),
- child: SizedBox(
- height: 100,
- width: 150,
- child: LightboxHero(
- message: message,
- src: resolvedSrc,
- child: RealmContentNetworkImage(
- resolvedSrc,
- filterQuality: FilterQuality.medium)))))));
+ child: Padding(
+ padding: const EdgeInsets.all(1),
+ child: SizedBox(
+ height: 100,
+ width: 150,
+ child: LightboxHero(
+ message: message,
+ src: resolvedSrc,
+ child: RealmContentNetworkImage(
+ resolvedSrc,
+ filterQuality: FilterQuality.medium))))))));
}
}
From 72efa3e559768d6080972c90288d329417381917 Mon Sep 17 00:00:00 2001
From: Shu Chen
Date: Mon, 18 Dec 2023 20:41:33 +0000
Subject: [PATCH 4/5] content: Handle clusters of images in
parseBlockContentList
---
lib/model/content.dart | 34 +++++++--
lib/widgets/content.dart | 17 ++++-
test/model/content_test.dart | 121 ++++++++++++++++++++++++++++++---
test/widgets/content_test.dart | 71 +++++++++++++++++++
4 files changed, 228 insertions(+), 15 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 925a3e64f3..f4d0faf44c 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -309,6 +309,17 @@ class MathBlockNode extends BlockContentNode {
}
}
+class ImageNodeList extends BlockContentNode {
+ const ImageNodeList(this.images, {super.debugHtmlNode});
+
+ final List images;
+
+ @override
+ List debugDescribeChildren() {
+ return images.map((node) => node.toDiagnosticsNode()).toList();
+ }
+}
+
class ImageNode extends BlockContentNode {
const ImageNode({super.debugHtmlNode, required this.srcUrl});
@@ -1031,13 +1042,26 @@ class _ZulipContentParser {
List parseBlockContentList(dom.NodeList nodes) {
assert(_debugParserContext == _ParserContext.block);
- final acceptedNodes = nodes.where((node) {
+ final List result = [];
+ List imageNodes = [];
+ for (final node in nodes) {
// We get a bunch of newline Text nodes between paragraphs.
// A browser seems to ignore these; let's do the same.
- if (node is dom.Text && (node.text == '\n')) return false;
- return true;
- });
- return acceptedNodes.map(parseBlockContent).toList(growable: false);
+ if (node is dom.Text && (node.text == '\n')) continue;
+
+ final block = parseBlockContent(node);
+ if (block is ImageNode) {
+ imageNodes.add(block);
+ continue;
+ }
+ if (imageNodes.isNotEmpty) {
+ result.add(ImageNodeList(imageNodes));
+ imageNodes = [];
+ }
+ result.add(block);
+ }
+ if (imageNodes.isNotEmpty) result.add(ImageNodeList(imageNodes));
+ return result;
}
ZulipContent parse(String html) {
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index db6dd5b6d6..f22108ec15 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -84,6 +84,8 @@ class BlockContentList extends StatelessWidget {
return CodeBlock(node: node);
} else if (node is MathBlockNode) {
return MathBlock(node: node);
+ } else if (node is ImageNodeList) {
+ return MessageImageList(node: node);
} else if (node is ImageNode) {
return MessageImage(node: node);
} else if (node is UnimplementedBlockContentNode) {
@@ -230,6 +232,18 @@ class ListItemWidget extends StatelessWidget {
}
}
+class MessageImageList extends StatelessWidget {
+ const MessageImageList({super.key, required this.node});
+
+ final ImageNodeList node;
+
+ @override
+ Widget build(BuildContext context) {
+ return Wrap(
+ children: node.images.map((imageNode) => MessageImage(node: imageNode)).toList());
+ }
+}
+
class MessageImage extends StatelessWidget {
const MessageImage({super.key, required this.node});
@@ -239,7 +253,6 @@ class MessageImage extends StatelessWidget {
Widget build(BuildContext context) {
final message = InheritedMessage.of(context);
- // TODO(#193) multiple images in a row
// TODO image hover animation
final src = node.srcUrl;
@@ -251,7 +264,7 @@ class MessageImage extends StatelessWidget {
Navigator.of(context).push(getLightboxRoute(
context: context, message: message, src: resolvedSrc));
},
- child: Align(
+ child: UnconstrainedBox(
alignment: Alignment.centerLeft,
child: Padding(
// TODO clean up this padding by imitating web less precisely;
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index 9d8c4f22d0..8d22bcef44 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -256,6 +256,115 @@ class ContentExample {
'λ'
'
\n
\n',
[QuotationNode([MathBlockNode(texSource: r'\lambda')])]);
+
+ static const imageSingle = ContentExample(
+ 'single image',
+ "https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3",
+ '', [
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3'),
+ ]),
+ ]);
+
+ static const imageCluster = ContentExample(
+ 'multiple images',
+ "https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3\nhttps://chat.zulip.org/user_avatars/2/realm/icon.png?version=4",
+ ''
+ 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3
\n'
+ 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=4
\n'
+ ''
+ '', [
+ ParagraphNode(links: null, nodes: [
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3', nodes: [TextNode('https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3')]),
+ LineBreakInlineNode(),
+ TextNode('\n'),
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=4', nodes: [TextNode('https://chat.zulip.org/user_avatars/2/realm/icon.png?version=4')]),
+ ]),
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/f535ba07f95b99a83aa48e44fd62bbb6c6cf6615/68747470733a2f2f636861742e7a756c69702e6f72672f757365725f617661746172732f322f7265616c6d2f69636f6e2e706e673f76657273696f6e3d33'),
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/8f63bc2632a0e41be3f457d86c077e61b4a03e7e/68747470733a2f2f636861742e7a756c69702e6f72672f757365725f617661746172732f322f7265616c6d2f69636f6e2e706e673f76657273696f6e3d34'),
+ ]),
+ ]);
+
+ static const imageClusterThenContent = ContentExample(
+ 'content after image cluster',
+ "https://chat.zulip.org/user_avatars/2/realm/icon.png\nhttps://chat.zulip.org/user_avatars/2/realm/icon.png?version=2\n\nmore content",
+ 'content '
+ 'icon.png '
+ 'icon.png
\n'
+ ''
+ ''
+ 'more content
', [
+ ParagraphNode(links: null, nodes: [
+ TextNode('content '),
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
+ TextNode(' '),
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
+ ]),
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
+ ]),
+ ParagraphNode(links: null, nodes: [
+ TextNode('more content'),
+ ]),
+ ]);
+
+ static const imageMultipleClusters = ContentExample(
+ 'multiple clusters of images',
+ "https://en.wikipedia.org/static/images/icons/wikipedia.png\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=1\n\nTest\n\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=2\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=3",
+ ''
+ 'https://en.wikipedia.org/static/images/icons/wikipedia.png
\n' 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=1
\n'
+ ''
+ ''
+ 'Test
\n'
+ ''
+ 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=2
\n'
+ 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=3
\n'
+ ''
+ '', [
+ ParagraphNode(links: null, nodes: [
+ LinkNode(url: 'https://en.wikipedia.org/static/images/icons/wikipedia.png', nodes: [TextNode('https://en.wikipedia.org/static/images/icons/wikipedia.png')]),
+ LineBreakInlineNode(),
+ TextNode('\n'),
+ LinkNode(url: 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=1', nodes: [TextNode('https://en.wikipedia.org/static/images/icons/wikipedia.png?v=1')]),
+ ]),
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/34b2695ca83af76204b0b25a8f2019ee35ec38fa/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e67'),
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/d200fb112aaccbff9df767373a201fa59601f362/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d31'),
+ ]),
+ ParagraphNode(links: null, nodes: [
+ TextNode('Test'),
+ ]),
+ ParagraphNode(links: null, nodes: [
+ LinkNode(url: 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=2', nodes: [TextNode('https://en.wikipedia.org/static/images/icons/wikipedia.png?v=2')]),
+ LineBreakInlineNode(),
+ TextNode('\n'),
+ LinkNode(url: 'https://en.wikipedia.org/static/images/icons/wikipedia.png?v=3', nodes: [TextNode('https://en.wikipedia.org/static/images/icons/wikipedia.png?v=3')]),
+ ]),
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/c4db87e81348dac94eacaa966b46d968b34029cc/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d32'),
+ ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33'),
+ ]),
+ ]);
}
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -576,14 +685,10 @@ void main() {
testParseExample(ContentExample.mathBlock);
testParseExample(ContentExample.mathBlockInQuote);
- testParse('parse image',
- // "https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3"
- '', const [
- ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3'),
- ]);
+ testParseExample(ContentExample.imageSingle);
+ testParseExample(ContentExample.imageCluster);
+ testParseExample(ContentExample.imageClusterThenContent);
+ testParseExample(ContentExample.imageMultipleClusters);
testParse('parse nested lists, quotes, headings, code blocks',
// "1. > ###### two\n > * three\n\n four"
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 9e8b9789b6..c13d886673 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -252,6 +252,77 @@ void main() {
tester.widget(find.textContaining(RegExp(r'^(Tue, Jan 30|Wed, Jan 31), 2024, \d+:\d\d [AP]M$')));
});
+ group('MessageImages', () {
+ final message = eg.streamMessage();
+
+ Future prepareContent(WidgetTester tester, String html) async {
+ addTearDown(testBinding.reset);
+
+ await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
+ final httpClient = FakeImageHttpClient();
+
+ debugNetworkImageHttpClientProvider = () => httpClient;
+ httpClient.request.response
+ ..statusCode = HttpStatus.ok
+ ..content = kSolidBlueAvatar;
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: GlobalStoreWidget(
+ child: PerAccountStoreWidget(
+ accountId: eg.selfAccount.id,
+ child: MessageContent(
+ message: message,
+ content: parseContent(html)))))));
+ await tester.pump(); // global store
+ await tester.pump(); // per-account store
+ debugNetworkImageHttpClientProvider = null;
+ }
+
+ testWidgets('single image', (tester) async {
+ const example = ContentExample.imageSingle;
+ await prepareContent(tester, example.html);
+ final expectedImages = (example.expectedNodes[0] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
+
+ testWidgets('multiple images', (tester) async {
+ const example = ContentExample.imageCluster;
+ await prepareContent(tester, example.html);
+ final expectedImages = (example.expectedNodes[1] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
+
+ testWidgets('content after image cluster', (tester) async {
+ const example = ContentExample.imageClusterThenContent;
+ await prepareContent(tester, example.html);
+ final expectedImages = (example.expectedNodes[1] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
+
+ testWidgets('multiple clusters of images', (tester) async {
+ const example = ContentExample.imageMultipleClusters;
+ await prepareContent(tester, example.html);
+ final expectedImages = (example.expectedNodes[1] as ImageNodeList).images
+ + (example.expectedNodes[4] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
+ });
+
group('RealmContentNetworkImage', () {
final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey);
From 61032ad5b93931d124f71ceaf82529a4eb31145a Mon Sep 17 00:00:00 2001
From: Shu Chen
Date: Thu, 25 Jan 2024 13:05:51 +0000
Subject: [PATCH 5/5] content: Handle clusters of images in
parseImplicitParagraphBlockContentList
Fixes: #193
---
lib/model/content.dart | 21 ++++++++++-
lib/widgets/content.dart | 4 ++
test/model/content_test.dart | 68 ++++++++++++++++++++++++++++++++++
test/widgets/content_test.dart | 22 +++++++++++
4 files changed, 114 insertions(+), 1 deletion(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index f4d0faf44c..19317c2db9 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -1016,6 +1016,7 @@ class _ZulipContentParser {
assert(_debugParserContext == _ParserContext.block);
final List result = [];
final List currentParagraph = [];
+ List imageNodes = [];
void consumeParagraph() {
final parsed = parseBlockInline(currentParagraph);
result.add(ParagraphNode(
@@ -1029,13 +1030,31 @@ class _ZulipContentParser {
if (node is dom.Text && (node.text == '\n')) continue;
if (_isPossibleInlineNode(node)) {
+ if (imageNodes.isNotEmpty) {
+ result.add(ImageNodeList(imageNodes));
+ imageNodes = [];
+ // In a context where paragraphs are implicit it should be impossible
+ // to have more paragraph content after image previews.
+ result.add(UnimplementedBlockContentNode(htmlNode: node));
+ continue;
+ }
currentParagraph.add(node);
continue;
}
if (currentParagraph.isNotEmpty) consumeParagraph();
- result.add(parseBlockContent(node));
+ final block = parseBlockContent(node);
+ if (block is ImageNode) {
+ imageNodes.add(block);
+ continue;
+ }
+ if (imageNodes.isNotEmpty) {
+ result.add(ImageNodeList(imageNodes));
+ imageNodes = [];
+ }
+ result.add(block);
}
if (currentParagraph.isNotEmpty) consumeParagraph();
+ if (imageNodes.isNotEmpty) result.add(ImageNodeList(imageNodes));
return result;
}
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index f22108ec15..e2a45923eb 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -87,6 +87,10 @@ class BlockContentList extends StatelessWidget {
} else if (node is ImageNodeList) {
return MessageImageList(node: node);
} else if (node is ImageNode) {
+ assert(false,
+ "[ImageNode] not allowed in [BlockContentList]. "
+ "It should be wrapped in [ImageNodeList]."
+ );
return MessageImage(node: node);
} else if (node is UnimplementedBlockContentNode) {
return Text.rich(_errorUnimplemented(node));
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index 8d22bcef44..4b00a42510 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -365,6 +365,71 @@ class ContentExample {
ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33'),
]),
]);
+
+ static const imageInImplicitParagraph = ContentExample(
+ 'image as immediate child in implicit paragraph',
+ "* https://chat.zulip.org/user_avatars/2/realm/icon.png",
+ '', [
+ ListNode(ListStyle.unordered, [[
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
+ ]),
+ ]]),
+ ]);
+
+ static const imageClusterInImplicitParagraph = ContentExample(
+ 'image cluster in implicit paragraph',
+ "* [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png) [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2)",
+ '', [
+ ListNode(ListStyle.unordered, [[
+ ParagraphNode(wasImplicit: true, links: null, nodes: [
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
+ TextNode(' '),
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
+ ]),
+ ImageNodeList([
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
+ ]),
+ ]]),
+ ]);
+
+ static final imageClusterInImplicitParagraphThenContent = ContentExample(
+ 'impossible content after image cluster in implicit paragraph',
+ // Image previews are always inserted at the end of the paragraph
+ // so it would be impossible to have content after.
+ null,
+ '\n'
+ '- '
+ 'icon.png '
+ ''
+ 'more text
\n
', [
+ ListNode(ListStyle.unordered, [[
+ const ParagraphNode(wasImplicit: true, links: null, nodes: [
+ LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
+ TextNode(' '),
+ ]),
+ const ImageNodeList([
+ ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
+ ]),
+ blockUnimplemented('more text'),
+ ]]),
+ ]);
}
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -689,6 +754,9 @@ void main() {
testParseExample(ContentExample.imageCluster);
testParseExample(ContentExample.imageClusterThenContent);
testParseExample(ContentExample.imageMultipleClusters);
+ testParseExample(ContentExample.imageInImplicitParagraph);
+ testParseExample(ContentExample.imageClusterInImplicitParagraph);
+ testParseExample(ContentExample.imageClusterInImplicitParagraphThenContent);
testParse('parse nested lists, quotes, headings, code blocks',
// "1. > ###### two\n > * three\n\n four"
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index c13d886673..0f130f9229 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -321,6 +321,28 @@ void main() {
check(images.map((i) => i.src.toString()).toList())
.deepEquals(expectedImages.map((n) => n.srcUrl));
});
+
+ testWidgets('image as immediate child in implicit paragraph', (tester) async {
+ const example = ContentExample.imageInImplicitParagraph;
+ await prepareContent(tester, example.html);
+ final expectedImages = ((example.expectedNodes[0] as ListNode)
+ .items[0][0] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
+
+ testWidgets('image cluster in implicit paragraph', (tester) async {
+ const example = ContentExample.imageClusterInImplicitParagraph;
+ await prepareContent(tester, example.html);
+ final expectedImages = ((example.expectedNodes[0] as ListNode)
+ .items[0][1] as ImageNodeList).images;
+ final images = tester.widgetList(
+ find.byType(RealmContentNetworkImage));
+ check(images.map((i) => i.src.toString()).toList())
+ .deepEquals(expectedImages.map((n) => n.srcUrl));
+ });
});
group('RealmContentNetworkImage', () {