From c9f4e8d3d889bbc745205954518fc0c0d316a6b8 Mon Sep 17 00:00:00 2001 From: Tristan Katana <50181095+felixmuinde@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:52:40 +0300 Subject: [PATCH] feat(mobile): render assistant messages as markdown (#1405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mobile): render assistant messages as markdown, keep user text plain Add flutter_markdown dependency and conditionally render chat bubbles: - User messages use plain Text to avoid formatting markdown characters - Assistant messages use MarkdownBody with styled headings, bold, italic, lists and code blocks matching the existing color scheme - Bump Dart SDK constraint to >=3.3.0 to satisfy flutter_markdown 0.7.2 * fix(mobile): address markdown rendering review comments - Extract MarkdownStyleSheet into _markdownStyle() helper to avoid rebuilding TextStyles on every message render - Replace deprecated imageBuilder with sizedImageBuilder; block http/https image URIs to prevent unsolicited remote fetches from AI-generated content - Commit updated pubspec.lock with flutter_markdown 0.7.2 resolved Co-Authored-By: Claude Sonnet 4.6 * Fix tests --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Juan José Mata --- .../lib/screens/chat_conversation_screen.dart | 44 ++++++++++++++++--- mobile/pubspec.lock | 32 ++++++++++---- mobile/pubspec.yaml | 3 +- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/mobile/lib/screens/chat_conversation_screen.dart b/mobile/lib/screens/chat_conversation_screen.dart index e5e85d264..258748df9 100644 --- a/mobile/lib/screens/chat_conversation_screen.dart +++ b/mobile/lib/screens/chat_conversation_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; import '../models/chat.dart'; import '../providers/auth_provider.dart'; @@ -414,6 +415,22 @@ class _MessageBubble extends StatelessWidget { required this.formatTime, }); + /// Builds the markdown stylesheet once per render context instead of inline, + /// avoiding redundant TextStyle allocations per message bubble. + MarkdownStyleSheet _markdownStyle(BuildContext context) { + final color = Theme.of(context).colorScheme.onSurfaceVariant; + return MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + p: TextStyle(color: color), + strong: TextStyle(color: color, fontWeight: FontWeight.bold), + em: TextStyle(color: color, fontStyle: FontStyle.italic), + listBullet: TextStyle(color: color), + h1: TextStyle(color: color, fontSize: 20, fontWeight: FontWeight.bold), + h2: TextStyle(color: color, fontSize: 18, fontWeight: FontWeight.bold), + h3: TextStyle(color: color, fontSize: 16, fontWeight: FontWeight.bold), + code: TextStyle(color: color, fontFamily: 'monospace'), + ); + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -455,14 +472,27 @@ class _MessageBubble extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - message.content, - style: TextStyle( - color: isUser - ? colorScheme.onPrimary - : colorScheme.onSurfaceVariant, + if (isUser) + Text( + message.content, + style: TextStyle( + color: colorScheme.onPrimary, + ), + ) + else + MarkdownBody( + data: message.content, + selectable: false, + softLineBreak: true, + styleSheet: _markdownStyle(context), + sizedImageBuilder: (config) { + // Block remote images to prevent unsolicited network requests. + if (config.uri.scheme == 'http' || config.uri.scheme == 'https') { + return const SizedBox.shrink(); + } + return Image.asset(config.uri.toString()); + }, ), - ), if (message.toolCalls != null && message.toolCalls!.isNotEmpty) Padding( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index b00dc2ebb..b7dd44a3f 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -198,6 +198,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" + url: "https://pub.dev" + source: hosted + version: "0.7.7+1" flutter_secure_storage: dependency: "direct main" description: @@ -344,22 +352,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 + url: "https://pub.dev" + source: hosted + version: "7.3.1" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -657,10 +673,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" typed_data: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index b81f489e0..d65c19585 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 0.6.9+20260402 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' flutter: '>=3.27.0' dependencies: @@ -24,6 +24,7 @@ dependencies: url_launcher: ^6.2.5 flutter_svg: ^2.2.0 package_info_plus: ^8.0.0 + flutter_markdown: ^0.7.2 dev_dependencies: flutter_test: