From b174de77012a0fd89077e161a04ac4f67d178709 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:18:15 +0530 Subject: [PATCH] refactor: add support for displaying user-generated image files in message bubbles --- .../chat/widgets/user_message_bubble.dart | 215 +++++++++++++++++- 1 file changed, 204 insertions(+), 11 deletions(-) diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 9a26c99..3a885fd 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -70,16 +70,43 @@ class _UserMessageBubbleState extends ConsumerState ); } - Widget _buildImageLayout(int imageCount) { + Widget _buildUserFileImages() { + if (widget.message.files == null || widget.message.files!.isEmpty) { + return const SizedBox.shrink(); + } + + final imageFiles = widget.message.files! + .where( + (file) => + file is Map && file['type'] == 'image' && file['url'] != null, + ) + .toList(); + + if (imageFiles.isEmpty) { + return const SizedBox.shrink(); + } + + final imageCount = imageFiles.length; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOut, + child: _buildFileImageLayout(imageFiles, imageCount), + ); + } + + Widget _buildFileImageLayout(List imageFiles, int imageCount) { if (imageCount == 1) { - // Single image - larger display + final String imageUrl = imageFiles[0]['url'] as String; return Row( - key: ValueKey('user_single_${widget.message.attachmentIds![0]}'), + key: ValueKey('user_file_single_$imageUrl'), mainAxisAlignment: MainAxisAlignment.end, children: [ Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppBorderRadius.messageBubble), + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), boxShadow: [ BoxShadow( color: context.conduitTheme.cardShadow.withValues(alpha: 0.1), @@ -89,11 +116,161 @@ class _UserMessageBubbleState extends ConsumerState ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(AppBorderRadius.messageBubble), + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + child: EnhancedImageAttachment( + attachmentId: imageUrl, + isUserMessage: true, + isMarkdownFormat: false, + constraints: const BoxConstraints( + maxWidth: 280, + maxHeight: 350, + ), + disableAnimation: widget.isStreaming, + ), + ), + ), + ], + ); + } else if (imageCount == 2) { + return Row( + key: ValueKey( + 'user_file_double_${imageFiles.map((e) => e['url']).join('_')}', + ), + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: imageFiles.asMap().entries.map((entry) { + final index = entry.key; + final String imageUrl = entry.value['url'] as String; + return Padding( + padding: EdgeInsets.only(left: index == 0 ? 0 : Spacing.xs), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + boxShadow: [ + BoxShadow( + color: context.conduitTheme.cardShadow.withValues( + alpha: 0.08, + ), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + child: EnhancedImageAttachment( + key: ValueKey('user_file_attachment_$imageUrl'), + attachmentId: imageUrl, + isUserMessage: true, + isMarkdownFormat: false, + constraints: const BoxConstraints( + maxWidth: 135, + maxHeight: 180, + ), + disableAnimation: widget.isStreaming, + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ); + } else { + return Row( + key: ValueKey( + 'user_file_grid_${imageFiles.map((e) => e['url']).join('_')}', + ), + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 280), + child: Wrap( + alignment: WrapAlignment.end, + spacing: Spacing.xs, + runSpacing: Spacing.xs, + children: imageFiles.map((file) { + final String imageUrl = file['url'] as String; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + boxShadow: [ + BoxShadow( + color: context.conduitTheme.cardShadow.withValues( + alpha: 0.06, + ), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: EnhancedImageAttachment( + key: ValueKey('user_file_grid_attachment_$imageUrl'), + attachmentId: imageUrl, + isUserMessage: true, + isMarkdownFormat: false, + constraints: BoxConstraints( + maxWidth: imageCount == 3 ? 135 : 90, + maxHeight: imageCount == 3 ? 135 : 90, + ), + disableAnimation: widget.isStreaming, + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ); + } + } + + Widget _buildImageLayout(int imageCount) { + if (imageCount == 1) { + // Single image - larger display + return Row( + key: ValueKey('user_single_${widget.message.attachmentIds![0]}'), + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + boxShadow: [ + BoxShadow( + color: context.conduitTheme.cardShadow.withValues(alpha: 0.1), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), child: EnhancedImageAttachment( attachmentId: widget.message.attachmentIds![0], isUserMessage: true, - constraints: const BoxConstraints(maxWidth: 280, maxHeight: 350), + constraints: const BoxConstraints( + maxWidth: 280, + maxHeight: 350, + ), disableAnimation: widget.isStreaming, ), ), @@ -110,24 +287,32 @@ class _UserMessageBubbleState extends ConsumerState Flexible( child: Row( mainAxisSize: MainAxisSize.min, - children: widget.message.attachmentIds!.asMap().entries.map((entry) { + children: widget.message.attachmentIds!.asMap().entries.map(( + entry, + ) { final index = entry.key; final attachmentId = entry.value; return Padding( padding: EdgeInsets.only(left: index == 0 ? 0 : Spacing.xs), child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppBorderRadius.messageBubble), + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), boxShadow: [ BoxShadow( - color: context.conduitTheme.cardShadow.withValues(alpha: 0.08), + color: context.conduitTheme.cardShadow.withValues( + alpha: 0.08, + ), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(AppBorderRadius.messageBubble), + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), child: EnhancedImageAttachment( key: ValueKey('user_attachment_$attachmentId'), attachmentId: attachmentId, @@ -165,7 +350,9 @@ class _UserMessageBubbleState extends ConsumerState borderRadius: BorderRadius.circular(AppBorderRadius.md), boxShadow: [ BoxShadow( - color: context.conduitTheme.cardShadow.withValues(alpha: 0.06), + color: context.conduitTheme.cardShadow.withValues( + alpha: 0.06, + ), blurRadius: 3, offset: const Offset(0, 1), ), @@ -227,6 +414,11 @@ class _UserMessageBubbleState extends ConsumerState widget.message.attachmentIds != null && widget.message.attachmentIds!.isNotEmpty; final hasText = widget.message.content.isNotEmpty; + final hasGeneratedImages = + widget.message.files != null && + (widget.message.files as List).any( + (f) => f is Map && f['type'] == 'image' && f['url'] != null, + ); return GestureDetector( onLongPress: () => _toggleActions(), @@ -243,6 +435,7 @@ class _UserMessageBubbleState extends ConsumerState children: [ // Display images outside and above the text bubble (iMessage style) if (hasImages) ...[_buildUserAttachmentImages()], + if (hasGeneratedImages) ...[_buildUserFileImages()], // Display text bubble if there's text content if (hasText) const SizedBox(height: Spacing.xs),