@@ -333,6 +333,10 @@ class BlockContentList extends StatelessWidget {
333333
334334 @override
335335 Widget build (BuildContext context) {
336+ // Filter out LineBreakNode to determine if there's only one meaningful content block
337+ final meaningfulNodes = nodes.where ((n) => n is ! LineBreakNode ).toList ();
338+ final isSoleContent = meaningfulNodes.length == 1 ;
339+
336340 return Column (crossAxisAlignment: CrossAxisAlignment .stretch, children: [
337341 ...nodes.map ((node) {
338342 return switch (node) {
@@ -341,7 +345,7 @@ class BlockContentList extends StatelessWidget {
341345 // just use an empty Text.
342346 const Text ('' ),
343347 ThematicBreakNode () => const ThematicBreak (),
344- ParagraphNode () => Paragraph (node: node),
348+ ParagraphNode () => Paragraph (node: node, isSoleContent : isSoleContent ),
345349 HeadingNode () => Heading (node: node),
346350 QuotationNode () => Quotation (node: node),
347351 ListNode () => ListNodeWidget (node: node),
@@ -383,6 +387,27 @@ class BlockContentList extends StatelessWidget {
383387 }
384388}
385389
390+ // Return true when the paragraph contains one or more emoji nodes and
391+ // otherwise consists only of whitespace text nodes. This matches the
392+ // "emoji-only paragraph" rule used to render large emoji on web.
393+ bool _isEmojiOnlyParagraph (ParagraphNode p) {
394+ if (p.nodes.isEmpty) return false ;
395+ var foundEmoji = false ;
396+ for (final n in p.nodes) {
397+ if (n is TextNode ) {
398+ if (n.text.trim ().isEmpty) continue ;
399+ return false ;
400+ }
401+ if (n is UnicodeEmojiNode || n is ImageEmojiNode ) {
402+ foundEmoji = true ;
403+ continue ;
404+ }
405+ // Any other inline node disqualifies the paragraph
406+ return false ;
407+ }
408+ return foundEmoji;
409+ }
410+
386411class ThematicBreak extends StatelessWidget {
387412 const ThematicBreak ({super .key});
388413
@@ -400,19 +425,30 @@ class ThematicBreak extends StatelessWidget {
400425}
401426
402427class Paragraph extends StatelessWidget {
403- const Paragraph ({super .key, required this .node});
428+ const Paragraph ({super .key, required this .node, this .isSoleContent = false });
404429
405430 final ParagraphNode node;
431+ final bool isSoleContent;
406432
407433 @override
408434 Widget build (BuildContext context) {
409435 // Empty paragraph winds up with zero height.
410436 // The paragraph has vertical CSS margins, but those have no effect.
411437 if (node.nodes.isEmpty) return const SizedBox ();
412438
439+ // Detect emoji-only paragraphs: a paragraph that contains one or more
440+ // emoji nodes (Unicode or image emoji), and otherwise only whitespace.
441+ // When present, render emoji at a larger size to match web behavior.
442+ // Only apply this when the paragraph is the sole content block.
443+ final baseStyle = DefaultTextStyle .of (context).style;
444+ final isEmojiOnly = isSoleContent && _isEmojiOnlyParagraph (node);
445+ final effectiveStyle = isEmojiOnly
446+ ? baseStyle.copyWith (fontSize: baseStyle.fontSize! * 2 )
447+ : baseStyle;
448+
413449 final text = _buildBlockInlineContainer (
414450 node: node,
415- style: DefaultTextStyle . of (context).style ,
451+ style: effectiveStyle ,
416452 );
417453
418454 // If the paragraph didn't actually have a `p` element in the HTML,
@@ -1267,18 +1303,21 @@ class MessageImageEmoji extends StatelessWidget {
12671303 Widget build (BuildContext context) {
12681304 final store = PerAccountStoreWidget .of (context);
12691305 final resolvedSrc = store.tryResolveUrl (node.src);
1270-
1271- const size = 20.0 ;
1306+ // Make image emoji scale with the ambient font size so they match
1307+ // Unicode emoji rendered via text spans. Use the current DefaultTextStyle
1308+ // fontSize as the reference.
1309+ final ambientFontSize = DefaultTextStyle .of (context).style.fontSize ?? kBaseFontSize;
1310+ final size = ambientFontSize;
12721311
12731312 return Stack (
12741313 alignment: Alignment .center,
12751314 clipBehavior: Clip .none,
12761315 children: [
1277- const SizedBox (width: size, height: kBaseFontSize ),
1316+ SizedBox (width: size, height: ambientFontSize ),
12781317 Positioned (
1279- // Web's css makes this seem like it should be -0.5, but that looks
1280- // too low .
1281- top: - 1.5 ,
1318+ // Keep a small upward offset similar to previous value, scaled
1319+ // to current font size .
1320+ top: - 1.5 * ambientFontSize / kBaseFontSize ,
12821321 child: resolvedSrc == null ? const SizedBox .shrink () // TODO(log)
12831322 : RealmContentNetworkImage (
12841323 resolvedSrc,
0 commit comments