diff --git a/lib/src/controllers/chat/ds_audio_player.controller.dart b/lib/src/controllers/chat/ds_audio_player.controller.dart index 0f12ee37..53b86a42 100644 --- a/lib/src/controllers/chat/ds_audio_player.controller.dart +++ b/lib/src/controllers/chat/ds_audio_player.controller.dart @@ -6,6 +6,7 @@ class DSAudioPlayerController extends GetxController { final audioSpeed = RxDouble(1.0); final player = AudioPlayer(); final isInitialized = RxBool(false); + final isLoadingAudio = RxBool(false); /// Collects the data useful for displaying in a SeekBar widget. /// diff --git a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart index 5a0fb230..e2a7ad91 100644 --- a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart @@ -13,7 +13,6 @@ class DSImageMessageBubbleController extends GetxController { final maximumProgress = RxInt(0); final downloadProgress = RxInt(0); final localPath = RxnString(); - final String url; final String? mediaType; final bool shouldAuthenticate; diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index bade8679..a3d40bcf 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -109,7 +109,7 @@ class DSVideoMessageBubbleController { hasError.value = !isSuccess; } - _generateThumbnail(outputFile.path); + await _generateThumbnail(outputFile.path); } catch (_) { hasError.value = true; diff --git a/lib/src/extensions/future.extension.dart b/lib/src/extensions/future.extension.dart new file mode 100644 index 00000000..d626361b --- /dev/null +++ b/lib/src/extensions/future.extension.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; + +extension FutureExtension on Future { + Future trueWhile(Rx rxValue) { + rxValue.value = true; + + return whenComplete( + () => rxValue.value = false, + ); + } +} diff --git a/lib/src/models/ds_message_item.model.dart b/lib/src/models/ds_message_item.model.dart index 3070ce8e..c727a50c 100644 --- a/lib/src/models/ds_message_item.model.dart +++ b/lib/src/models/ds_message_item.model.dart @@ -30,7 +30,10 @@ class DSMessageItem { /// Used to define if a message detail (typicament a messages date and time) should be displayed or not bool? hideMessageDetail; - /// Creates a new Design System's [DSMessageItem] model + /// if the media message is uploading + bool isUploading; + + /// Creates a new Design System's [DSMessageItemModel] model DSMessageItem({ this.id, required this.date, @@ -41,6 +44,7 @@ class DSMessageItem { this.content, this.customer, this.hideMessageDetail, + this.isUploading = false, }); factory DSMessageItem.fromJson(Map json) { @@ -53,6 +57,7 @@ class DSMessageItem { content: json['content'], status: DSDeliveryReportStatus.unknown.getValue(json['status']), hideMessageDetail: json['hideMessageDetail'], + isUploading: json['isUploading'] ?? false, ); if (json.containsKey('customer')) { diff --git a/lib/src/widgets/animations/ds_uploading.widget.dart b/lib/src/widgets/animations/ds_uploading.widget.dart new file mode 100644 index 00000000..dc9a9b83 --- /dev/null +++ b/lib/src/widgets/animations/ds_uploading.widget.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../themes/colors/ds_colors.theme.dart'; +import '../../themes/icons/ds_icons.dart'; + +class DSUploading extends StatefulWidget { + final double size; + final Color color; + + const DSUploading({ + super.key, + this.size = 24.0, + this.color = DSColors.neutralLightSnow, + }); + + @override + State createState() => _DSUploadingState(); +} + +class _DSUploadingState extends State { + final _visible = RxBool(false); + + @override + void initState() { + super.initState(); + Timer.periodic( + const Duration(seconds: 1), + (timer) { + _visible.value = !_visible.value; + }, + ); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Obx( + () => AnimatedOpacity( + opacity: _visible.value ? 1.0 : 0.0, + duration: const Duration(seconds: 1), + child: Icon( + DSIcons.upload_outline, + color: widget.color, + size: widget.size, + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 4718cdb3..7aaeb48b 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -7,6 +7,7 @@ import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import '../../../controllers/chat/ds_audio_player.controller.dart'; +import '../../../extensions/future.extension.dart'; import '../../../services/ds_auth.service.dart'; import '../../../services/ds_file.service.dart'; import '../../../services/ds_media_format.service.dart'; @@ -112,7 +113,9 @@ class _DSAudioPlayerState extends State ); try { - await _loadAudio(); + await _loadAudio().trueWhile( + _controller.isLoadingAudio, + ); _controller.isInitialized.value = true; } catch (_) { @@ -214,7 +217,8 @@ class _DSAudioPlayerState extends State ? _controller.player.play : () => {}, isLoading: [ProcessingState.loading, ProcessingState.buffering] - .contains(processingState), + .contains(processingState) || + _controller.isLoadingAudio.value, color: _controller.isInitialized.value ? widget.controlForegroundColor : DSColors.contentDisable, diff --git a/lib/src/widgets/chat/ds_delivery_report_icon.widget.dart b/lib/src/widgets/chat/ds_delivery_report_icon.widget.dart index 26484d61..d112b954 100644 --- a/lib/src/widgets/chat/ds_delivery_report_icon.widget.dart +++ b/lib/src/widgets/chat/ds_delivery_report_icon.widget.dart @@ -26,6 +26,12 @@ class DSDeliveryReportIcon extends StatelessWidget { const String path = 'assets/images'; switch (deliveryStatus) { + case DSDeliveryReportStatus.accepted: + return _getIcon( + '$path/check.svg', + DSColors.neutralMediumElephant, + ); + case DSDeliveryReportStatus.failed: return DSCaptionSmallText( 'Falha ao enviar mensagem.', @@ -67,9 +73,10 @@ class DSDeliveryReportIcon extends StatelessWidget { ); default: - return _getIcon( - '$path/check.svg', - DSColors.neutralMediumElephant, + return const Icon( + DSIcons.clock_outline, + size: 16, + color: DSColors.contentDefault, ); } } diff --git a/lib/src/widgets/chat/ds_file_message_bubble.widget.dart b/lib/src/widgets/chat/ds_file_message_bubble.widget.dart index 9007c39b..89874e79 100644 --- a/lib/src/widgets/chat/ds_file_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_file_message_bubble.widget.dart @@ -8,6 +8,7 @@ import '../../models/ds_message_bubble_style.model.dart'; import '../../services/ds_auth.service.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../animations/ds_fading_circle_loading.widget.dart'; +import '../animations/ds_uploading.widget.dart'; import '../texts/ds_body_text.widget.dart'; import '../texts/ds_caption_small_text.widget.dart'; import '../utils/ds_file_extension_icon.util.dart'; @@ -22,6 +23,7 @@ class DSFileMessageBubble extends StatelessWidget { final List borderRadius; final DSMessageBubbleStyle style; final bool shouldAuthenticate; + final bool isUploading; final dynamic replyContent; /// Creates a Design System's [DSMessageBubble] used on files other than image, audio, or video @@ -31,34 +33,41 @@ class DSFileMessageBubble extends StatelessWidget { required this.url, required this.size, required this.filename, - this.replyContent, this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, DSMessageBubbleStyle? style, + this.isUploading = false, + this.replyContent, }) : style = style ?? DSMessageBubbleStyle(), controller = DSFileMessageBubbleController(); @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => controller.openFile( - url: url, - httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, - ), - child: DSMessageBubble( - borderRadius: borderRadius, - replyContent: replyContent, - padding: EdgeInsets.zero, - align: align, - style: style, - child: SizedBox( - height: 80.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildIcon(), - _buildText(), - ], + onTap: () => !isUploading + ? controller.openFile( + url: url, + httpHeaders: + shouldAuthenticate ? DSAuthService.httpHeaders : null, + ) + : null, + child: Opacity( + opacity: !isUploading ? 1 : .5, + child: DSMessageBubble( + replyContent: replyContent, + borderRadius: borderRadius, + padding: EdgeInsets.zero, + align: align, + style: style, + child: SizedBox( + height: 80.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildIcon(), + _buildText(), + ], + ), ), ), ), @@ -74,15 +83,19 @@ class DSFileMessageBubble extends StatelessWidget { ? const DSFadingCircleLoading( color: DSColors.neutralDarkRooftop, ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DSFileExtensionIcon( - filename: filename, - size: 40.0, + : !isUploading + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DSFileExtensionIcon( + filename: filename, + size: 40.0, + ), + ], + ) + : const DSUploading( + color: DSColors.neutralDarkRooftop, ), - ], - ), ), ); } @@ -108,6 +121,7 @@ class DSFileMessageBubble extends StatelessWidget { filename, color: color, textAlign: TextAlign.center, + isSelectable: true, shouldLinkify: false, ), Visibility( diff --git a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart index df2d0d0e..5455baee 100644 --- a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart @@ -7,6 +7,7 @@ import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_document_select.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; +import '../../utils/ds_utils.util.dart'; import '../texts/ds_caption_text.widget.dart'; import '../utils/ds_circular_progress.widget.dart'; import '../utils/ds_expanded_image.widget.dart'; @@ -30,11 +31,10 @@ class DSImageMessageBubble extends StatefulWidget { this.showSelect = false, this.onSelected, this.onOpenLink, - this.replyContent, this.shouldAuthenticate = false, this.mediaType, - this.imageMaxHeight, - this.imageMinHeight, + this.isUploading = false, + this.replyContent, }) : style = style ?? DSMessageBubbleStyle(); final DSAlign align; @@ -45,7 +45,6 @@ class DSImageMessageBubble extends StatefulWidget { final String? text; final String appBarText; final Uri? appBarPhotoUri; - final dynamic replyContent; final DSMessageBubbleStyle style; final List selectOptions; final bool showSelect; @@ -53,8 +52,8 @@ class DSImageMessageBubble extends StatefulWidget { final void Function(Map)? onOpenLink; final bool shouldAuthenticate; final String? mediaType; - final double? imageMaxHeight; - final double? imageMinHeight; + final bool isUploading; + final dynamic replyContent; @override State createState() => _DSImageMessageBubbleState(); @@ -84,10 +83,10 @@ class _DSImageMessageBubbleState extends State : DSColors.neutralLightSnow; return DSMessageBubble( - defaultMaxSize: 360.0, + replyContent: widget.replyContent, + defaultMaxSize: DSUtils.bubbleMinSize, shouldUseDefaultSize: true, align: widget.align, - replyContent: widget.replyContent, borderRadius: widget.borderRadius, padding: EdgeInsets.zero, hasSpacer: widget.hasSpacer, @@ -101,32 +100,28 @@ class _DSImageMessageBubbleState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx( - () => _controller.localPath.value != null - ? DSExpandedImage( - appBarText: widget.appBarText, - appBarPhotoUri: widget.appBarPhotoUri, - url: _controller.localPath.value!, - maxHeight: widget.imageMaxHeight != null - ? widget.imageMaxHeight! - : widget.showSelect - ? 200.0 - : double.infinity, - minHeight: widget.imageMinHeight != null - ? widget.imageMinHeight! - : widget.showSelect - ? 200.0 - : 0.0, - align: widget.align, - style: widget.style, - isLoading: false, - shouldAuthenticate: widget.shouldAuthenticate, - ) - : DSCircularProgress( - currentProgress: _controller.downloadProgress, - maximumProgress: _controller.maximumProgress, - foregroundColor: foregroundColor, - ), + SizedBox( + width: DSUtils.bubbleMinSize, + height: 200, + child: Obx( + () => _controller.localPath.value != null + ? DSExpandedImage( + width: DSUtils.bubbleMinSize, + appBarText: widget.appBarText, + appBarPhotoUri: widget.appBarPhotoUri, + url: _controller.localPath.value!, + align: widget.align, + style: widget.style, + isLoading: false, + shouldAuthenticate: widget.shouldAuthenticate, + isUploading: widget.isUploading, + ) + : DSCircularProgress( + currentProgress: _controller.downloadProgress, + maximumProgress: _controller.maximumProgress, + foregroundColor: foregroundColor, + ), + ), ), if ((widget.title?.isNotEmpty ?? false) || (widget.text?.isNotEmpty ?? false)) diff --git a/lib/src/widgets/chat/ds_location_message_bubble.widget.dart b/lib/src/widgets/chat/ds_location_message_bubble.widget.dart index f2a7d846..1a1b897f 100644 --- a/lib/src/widgets/chat/ds_location_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_location_message_bubble.widget.dart @@ -7,6 +7,7 @@ import '../../models/ds_message_bubble_style.model.dart'; import '../../services/ds_auth.service.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../../themes/icons/ds_icons.dart'; +import '../../utils/ds_utils.util.dart'; import '../animations/ds_spinner_loading.widget.dart'; import '../texts/ds_body_text.widget.dart'; import '../utils/ds_cached_network_image_view.widget.dart'; @@ -55,8 +56,8 @@ class DSLocationMessageBubble extends StatelessWidget { }, child: DSMessageBubble( shouldUseDefaultSize: true, - defaultMaxSize: 240.0, - defaultMinSize: 240.0, + defaultMaxSize: DSUtils.bubbleMinSize, + defaultMinSize: DSUtils.bubbleMinSize, borderRadius: borderRadius, replyContent: replyContent, padding: EdgeInsets.zero, diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index 2c3cfa8c..2e565598 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -10,6 +10,8 @@ import '../../../models/ds_message_bubble_style.model.dart'; import '../../../services/ds_auth.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../../../themes/icons/ds_icons.dart'; +import '../../../utils/ds_utils.util.dart'; +import '../../animations/ds_uploading.widget.dart'; import '../../buttons/ds_button.widget.dart'; import '../../utils/ds_circular_progress.widget.dart'; import '../ds_message_bubble.widget.dart'; @@ -39,9 +41,6 @@ class DSVideoMessageBubble extends StatefulWidget { /// Style for bubble final DSMessageBubbleStyle style; - // reply id message - final dynamic replyContent; - /// The video size final int mediaSize; @@ -51,6 +50,12 @@ class DSVideoMessageBubble extends StatefulWidget { /// Indicates if the HTTP Requests should be authenticated or not. final bool shouldAuthenticate; + /// Indicates if the video file is uploading + final bool isUploading; + + // reply id message + final dynamic replyContent; + /// Card for the purpose of triggering a video to play. /// /// This widget is intended to display a video card from a url passed in the [url] parameter. @@ -63,12 +68,13 @@ class DSVideoMessageBubble extends StatefulWidget { required this.appBarText, required this.mediaSize, this.appBarPhotoUri, - this.replyContent, this.type = 'video/mp4', this.text, this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, DSMessageBubbleStyle? style, + this.isUploading = false, + this.replyContent, }) : style = style ?? DSMessageBubbleStyle(); @override @@ -78,16 +84,22 @@ class DSVideoMessageBubble extends StatefulWidget { class _DSVideoMessageBubbleState extends State with AutomaticKeepAliveClientMixin { late final DSVideoMessageBubbleController _controller; + bool _isInitialized = false; @override void initState() { super.initState(); - _controller = DSVideoMessageBubbleController( - url: widget.url, - mediaSize: widget.mediaSize, - httpHeaders: widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, - type: widget.type, - ); + + if (!widget.isUploading) { + _controller = DSVideoMessageBubbleController( + url: widget.url, + mediaSize: widget.mediaSize, + httpHeaders: + widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, + type: widget.type, + ); + _isInitialized = true; + } } @override @@ -116,88 +128,112 @@ class _DSVideoMessageBubbleState extends State : DSColors.neutralDarkCity; return DSMessageBubble( - defaultMaxSize: 240, - shouldUseDefaultSize: true, + replyContent: widget.replyContent, align: widget.align, borderRadius: widget.borderRadius, padding: EdgeInsets.zero, style: widget.style, - replyContent: widget.replyContent, child: LayoutBuilder( builder: (_, constraints) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx( - () => Padding( - padding: widget.replyContent == null - ? EdgeInsets.zero - : const EdgeInsets.only(top: 8.0), - child: SizedBox( - height: 240, - width: 240, - child: _controller.hasError.value - ? const Icon( - DSIcons.video_broken_outline, - size: 80.0, - color: DSColors.neutralDarkRooftop, + SizedBox( + height: DSUtils.bubbleMinSize, + width: DSUtils.bubbleMinSize, + child: widget.isUploading + ? Stack( + children: [ + Image.file( + File( + widget.url, + ), + opacity: const AlwaysStoppedAnimation(.3), + width: DSUtils.bubbleMinSize, + height: DSUtils.bubbleMinSize, + fit: BoxFit.cover, + ), + const Positioned( + bottom: 10.0, + right: 10.0, + child: DSUploading(), + ), + const Center( + child: Icon( + DSIcons.video_outline, + color: DSColors.disabledBg, + size: 80.0, + ), + ), + ], + ) + : _isInitialized + ? Obx( + () => _controller.hasError.value + ? _buidErrorIcon() + : _controller.isLoadingThumbnail.value + ? const SizedBox.shrink() + : _controller.isDownloading.value + ? DSCircularProgress( + currentProgress: + _controller.downloadProgress, + maximumProgress: + _controller.maximumProgress, + foregroundColor: foregroundColor, + ) + : _controller.thumbnail.isEmpty + ? Center( + child: SizedBox( + height: 40, + child: DSButton( + leadingIcon: const Icon( + DSIcons.download_outline, + size: 20, + ), + backgroundColor: + buttonBackgroundColor, + foregroundColor: + buttonForegroundColor, + borderColor: + buttonBorderColor, + label: _controller.size(), + onPressed: + _controller.downloadVideo, + ), + ), + ) + : DSVideoBody( + align: widget.align, + appBarPhotoUri: + widget.appBarPhotoUri, + appBarText: widget.appBarText, + url: widget.url, + shouldAuthenticate: + widget.shouldAuthenticate, + thumbnail: Center( + child: Image.file( + File( + _controller.thumbnail.value, + ), + width: DSUtils.bubbleMinSize, + height: DSUtils.bubbleMinSize, + fit: BoxFit.cover, + ), + ), + ), ) - : _controller.isDownloading.value - ? DSCircularProgress( - currentProgress: _controller.downloadProgress, - maximumProgress: _controller.maximumProgress, - foregroundColor: foregroundColor, - ) - : _controller.thumbnail.isEmpty - ? Center( - child: SizedBox( - height: 40, - child: DSButton( - leadingIcon: const Icon( - DSIcons.download_outline, - size: 20, - ), - backgroundColor: buttonBackgroundColor, - foregroundColor: buttonForegroundColor, - borderColor: buttonBorderColor, - label: _controller.size(), - onPressed: _controller.downloadVideo, - ), - ), - ) - : DSVideoBody( - align: widget.align, - appBarPhotoUri: widget.appBarPhotoUri, - appBarText: widget.appBarText, - url: widget.url, - shouldAuthenticate: widget.shouldAuthenticate, - thumbnail: Center( - child: Image.file( - File( - _controller.thumbnail.value, - ), - width: 240, - height: 240, - fit: BoxFit.cover, - ), - ), - ), - ), - ), + : _buidErrorIcon(), ), if (widget.text?.isNotEmpty ?? false) - SizedBox( - width: 240, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - child: DSShowMoreText( - text: widget.text!, - align: widget.align, - style: widget.style, - maxWidth: constraints.maxWidth, - ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: DSShowMoreText( + text: widget.text!, + align: widget.align, + style: widget.style, + maxWidth: constraints.maxWidth, ), ), ], @@ -205,4 +241,10 @@ class _DSVideoMessageBubbleState extends State ), ); } + + Widget _buidErrorIcon() => const Icon( + DSIcons.video_broken_outline, + size: 80.0, + color: DSColors.neutralDarkRooftop, + ); } diff --git a/lib/src/widgets/utils/ds_card.widget.dart b/lib/src/widgets/utils/ds_card.widget.dart index e5cf9238..a776a245 100644 --- a/lib/src/widgets/utils/ds_card.widget.dart +++ b/lib/src/widgets/utils/ds_card.widget.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; @@ -44,6 +46,7 @@ class DSCard extends StatelessWidget { this.showQuickReplyOptions = false, this.showRequestLocationButton = false, this.replyContent, + this.isUploading = false, }) : style = style ?? DSMessageBubbleStyle(); final String type; @@ -60,6 +63,7 @@ class DSCard extends StatelessWidget { final bool showQuickReplyOptions; final bool showRequestLocationButton; final dynamic replyContent; + final bool isUploading; @override Widget build(BuildContext context) { @@ -262,7 +266,9 @@ class DSCard extends StatelessWidget { if (media.type.contains('audio')) { return DSAudioMessageBubble( - uri: Uri.parse(media.uri), + uri: media.uri.startsWith('http') + ? Uri.parse(media.uri) + : File(media.uri).uri, align: align, borderRadius: borderRadius, style: style, @@ -288,7 +294,7 @@ class DSCard extends StatelessWidget { style: style, shouldAuthenticate: shouldAuthenticate, mediaType: media.type, - imageMaxHeight: 300.0, + isUploading: isUploading, ); } else if (media.type.contains('video')) { return DSVideoMessageBubble( @@ -307,6 +313,7 @@ class DSCard extends StatelessWidget { style: style, mediaSize: size, shouldAuthenticate: shouldAuthenticate, + isUploading: isUploading, ); } else { return DSFileMessageBubble( @@ -319,6 +326,7 @@ class DSCard extends StatelessWidget { borderRadius: borderRadius, style: style, shouldAuthenticate: shouldAuthenticate, + isUploading: isUploading, ); } } diff --git a/lib/src/widgets/utils/ds_expanded_image.widget.dart b/lib/src/widgets/utils/ds_expanded_image.widget.dart index e411d8d4..36c72167 100644 --- a/lib/src/widgets/utils/ds_expanded_image.widget.dart +++ b/lib/src/widgets/utils/ds_expanded_image.widget.dart @@ -6,19 +6,20 @@ import 'package:get/get.dart'; import 'package:pinch_zoom/pinch_zoom.dart'; import '../../../blip_ds.dart'; +import '../animations/ds_uploading.widget.dart'; class DSExpandedImage extends StatelessWidget { final String appBarText; final String url; final BoxFit fit; final double width; - final double minHeight; - final double maxHeight; + final double height; final bool isLoading; final Uri? appBarPhotoUri; final DSMessageBubbleStyle style; final DSAlign align; final bool shouldAuthenticate; + final bool isUploading; final _error = RxBool(false); final _isAppBarVisible = RxBool(false); @@ -29,13 +30,13 @@ class DSExpandedImage extends StatelessWidget { required this.url, this.fit = BoxFit.cover, this.width = double.infinity, - this.maxHeight = double.infinity, - this.minHeight = 0.0, + this.height = double.infinity, this.isLoading = false, this.appBarPhotoUri, this.shouldAuthenticate = false, DSMessageBubbleStyle? style, DSAlign? align, + this.isUploading = false, }) : style = style ?? DSMessageBubbleStyle(), align = align ?? DSAlign.center; @@ -50,30 +51,40 @@ class DSExpandedImage extends StatelessWidget { _expandImage(); } }, - child: Container( - constraints: BoxConstraints( - maxHeight: maxHeight, - minHeight: minHeight, - ), - child: url.startsWith('http') - ? DSCachedNetworkImageView( - fit: fit, - width: width, - url: url, - placeholder: (_, __) => _buildLoading(), - onError: () => _error.value = true, - align: align, - style: style, - shouldAuthenticate: shouldAuthenticate, - ) - : Image.file( - File(url), - width: width, - fit: fit, - cacheWidth: 360, - errorBuilder: (_, __, ___) => _defaultErrorWidget(), - ), - ), + child: url.startsWith('http') + ? DSCachedNetworkImageView( + fit: fit, + width: width, + height: height, + url: url, + placeholder: (_, __) => _buildLoading(), + onError: () => _error.value = true, + align: align, + style: style, + shouldAuthenticate: shouldAuthenticate, + ) + : Stack( + children: [ + Image.file( + File(url), + width: width, + height: height, + fit: fit, + cacheWidth: 360, + errorBuilder: (_, __, ___) => _defaultErrorWidget(), + opacity: + isUploading ? const AlwaysStoppedAnimation(.3) : null, + ), + Positioned( + bottom: 10.0, + right: 10.0, + child: Visibility( + visible: isUploading, + child: const DSUploading(), + ), + ), + ], + ), ), ); diff --git a/lib/src/widgets/utils/ds_group_card.widget.dart b/lib/src/widgets/utils/ds_group_card.widget.dart index e662f91f..012e738b 100644 --- a/lib/src/widgets/utils/ds_group_card.widget.dart +++ b/lib/src/widgets/utils/ds_group_card.widget.dart @@ -261,6 +261,7 @@ class _DSGroupCardState extends State { onOpenLink: widget.onOpenLink, messageId: message.id, customer: message.customer, + isUploading: message.isUploading, ); final isLastMsg = msgCount == length; diff --git a/test/widgets/chat/goldens/ds_delivery_report_icon/ds_delivery_report_icon.png b/test/widgets/chat/goldens/ds_delivery_report_icon/ds_delivery_report_icon.png index 30441168..4776337d 100644 Binary files a/test/widgets/chat/goldens/ds_delivery_report_icon/ds_delivery_report_icon.png and b/test/widgets/chat/goldens/ds_delivery_report_icon/ds_delivery_report_icon.png differ