diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d38c08..83d93762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] * **Feat**: [454](https://github.com/SimformSolutionsPvtLtd/chatview/pull/454) :sparkles: Add configurable image fit option for AdaptiveImage widget +* **Feat**: [457](https://github.com/SimformSolutionsPvtLtd/chatview/pull/457) :sparkles: Add errorWidget support to ImageMessageConfiguration ## 3.1.0 diff --git a/lib/src/models/config_models/image_message_configuration.dart b/lib/src/models/config_models/image_message_configuration.dart index d7afd669..2f16fd0c 100644 --- a/lib/src/models/config_models/image_message_configuration.dart +++ b/lib/src/models/config_models/image_message_configuration.dart @@ -22,6 +22,16 @@ import 'package:chatview_utils/chatview_utils.dart'; import 'package:flutter/material.dart'; +/// Builds the widget shown when an image message fails to load. +/// +/// [imageUrl] is the image source (network url, file path, or data uri) and +/// [error] is the object thrown while loading it. +typedef ImageMessageErrorWidgetBuilder = Widget Function( + BuildContext context, + String imageUrl, + Object error, +); + class ImageMessageConfiguration { const ImageMessageConfiguration({ this.hideShareIcon = false, @@ -34,6 +44,7 @@ class ImageMessageConfiguration { this.margin, this.borderRadius, this.fit = BoxFit.cover, + this.errorBuilder, }); /// Provides configuration of share button while image message is appeared. @@ -68,6 +79,11 @@ class ImageMessageConfiguration { /// Defaults to [BoxFit.cover] (fills the bubble, center-cropped). Use /// [BoxFit.contain] to show the whole image without cropping. final BoxFit fit; + + /// Builds a custom widget shown when an image fails to load, with access to + /// the failing [imageUrl] and the [error]. Replaces the default broken-image + /// placeholder when set. + final ImageMessageErrorWidgetBuilder? errorBuilder; } class ShareIconConfiguration { diff --git a/lib/src/utils/constants/constants.dart b/lib/src/utils/constants/constants.dart index e3ad62fb..091353bf 100644 --- a/lib/src/utils/constants/constants.dart +++ b/lib/src/utils/constants/constants.dart @@ -30,6 +30,7 @@ const String imageUrlRegExpression = r'(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg)'; const String dateFormat = "yyyy-MM-dd"; const String couldNotLaunch = "Could not launch"; +const String couldNotLoadImage = "Couldn't load image"; const String heart = "\u{2764}"; const String faceWithTears = "\u{1F602}"; const String disappointedFace = "\u{1F625}"; diff --git a/lib/src/widgets/adaptive_image.dart b/lib/src/widgets/adaptive_image.dart index db32dd95..d383e73c 100644 --- a/lib/src/widgets/adaptive_image.dart +++ b/lib/src/widgets/adaptive_image.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:chatview/src/extensions/extensions.dart'; +import 'package:chatview/src/models/config_models/image_message_configuration.dart'; +import 'package:chatview/src/utils/constants/constants.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,6 +13,7 @@ class AdaptiveImage extends StatelessWidget { super.key, required this.imageUrl, this.fit = BoxFit.cover, + this.errorBuilder, }); final String imageUrl; @@ -18,6 +21,10 @@ class AdaptiveImage extends StatelessWidget { /// How the image is inscribed into its box. See [BoxFit]. final BoxFit fit; + /// Builds a custom error widget with access to the failing [imageUrl] and + /// [error]. Falls back to a built-in placeholder when null. + final ImageMessageErrorWidgetBuilder? errorBuilder; + @override Widget build(BuildContext context) { if (imageUrl.isUrl) { @@ -31,9 +38,7 @@ class AdaptiveImage extends StatelessWidget { progressIndicatorBuilder: (context, url, progress) => Center( child: CircularProgressIndicator(value: progress.progress), ), - errorWidget: (context, url, error) => const Center( - child: Icon(Icons.error_outline, size: 18), - ), + errorWidget: (context, _, error) => _buildError(context, error), ); } else if (imageUrl.fromMemory) { return Image.memory( @@ -42,14 +47,40 @@ class AdaptiveImage extends StatelessWidget { // Avoid a flicker to a blank frame while the next decode happens on // rebuild (e.g. during scrolling). gaplessPlayback: true, + errorBuilder: (context, error, stackTrace) => + _buildError(context, error), ); } else { return kIsWeb - ? Image.network(imageUrl, fit: fit) - : Image.file(File(imageUrl), fit: fit); + ? Image.network( + imageUrl, + fit: fit, + errorBuilder: (context, error, _) => _buildError(context, error), + ) + : Image.file( + File(imageUrl), + fit: fit, + errorBuilder: (context, error, _) => _buildError(context, error), + ); } } + Widget _buildError(BuildContext context, Object error) => + errorBuilder?.call(context, imageUrl, error) ?? + const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, size: 30, color: Colors.white70), + SizedBox(height: 8), + Text( + couldNotLoadImage, + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ], + ), + ); + /// Decodes the base64 payload of a `data:image` URI. Uint8List _decodeBase64(String value) { final startIndex = value.indexOf('base64,'); diff --git a/lib/src/widgets/image_message_view.dart b/lib/src/widgets/image_message_view.dart index eae1dfe0..e3f0c03b 100644 --- a/lib/src/widgets/image_message_view.dart +++ b/lib/src/widgets/image_message_view.dart @@ -172,6 +172,7 @@ class ImageMessageView extends StatelessWidget { child: AdaptiveImage( imageUrl: imageUrl, fit: imageMessageConfig?.fit ?? BoxFit.cover, + errorBuilder: imageMessageConfig?.errorBuilder, ), ), ),