diff --git a/CHANGELOG.md b/CHANGELOG.md index 314b686e..87e2b25e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.90 + +- [DSReplyContainer] Created the widget. +- [DSUploading] Created the widget. + ## 0.0.89 - [DSInteractiveButtonMessageBubble] Fixed border radius behavior. diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index db10c997..d783b8cf 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -21,6 +21,11 @@ export 'src/models/ds_message_bubble_avatar_config.model.dart' export 'src/models/ds_message_bubble_style.model.dart' show DSMessageBubbleStyle; export 'src/models/ds_message_item.model.dart' show DSMessageItem; +export 'src/models/ds_reply_content.model.dart' show DSReplyContent; +export 'src/models/ds_reply_content_in_reply_to.model.dart' + show DSReplyContentInReplyTo; +export 'src/models/ds_reply_content_replied.model.dart' + show DSReplyContentReplied; export 'src/models/ds_toast_props.model.dart' show DSToastProps; export 'src/models/interactive_message/ds_interactive_message.model.dart' show DSInteractiveMessage; @@ -135,6 +140,7 @@ export 'src/widgets/chat/ds_location_message_bubble.widget.dart' export 'src/widgets/chat/ds_message_bubble.widget.dart' show DSMessageBubble; export 'src/widgets/chat/ds_message_bubble_detail.widget.dart' show DSMessageBubbleDetail; +export 'src/widgets/chat/ds_reply_container.widget.dart' show DSReplyContainer; export 'src/widgets/chat/ds_request_location_bubble.widget.dart' show DSRequestLocationBubble; export 'src/widgets/chat/ds_survey_message_bubble.widget.dart' @@ -144,7 +150,8 @@ export 'src/widgets/chat/ds_text_message_bubble.widget.dart' export 'src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart' show DSUnsupportedContentMessageBubble; export 'src/widgets/chat/ds_url_preview.widget.dart' show DSUrlPreview; -export 'src/widgets/chat/ds_weblink.widget.dart' show DSWeblink; +export 'src/widgets/chat/ds_weblink_message_bubble.widget.dart' + show DSWeblinkMessageBubble; export 'src/widgets/chat/typing/ds_typing_dot_animation.widget.dart' show DSTypingDotAnimation; export 'src/widgets/chat/typing/ds_typing_message_bubble.widget.dart' 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/models/ds_reply_content.model.dart b/lib/src/models/ds_reply_content.model.dart new file mode 100644 index 00000000..8bb86101 --- /dev/null +++ b/lib/src/models/ds_reply_content.model.dart @@ -0,0 +1,20 @@ +import 'ds_reply_content_in_reply_to.model.dart'; +import 'ds_reply_content_replied.model.dart'; + +class DSReplyContent { + DSReplyContentReplied replied; + DSReplyContentInReplyTo inReplyTo; + + DSReplyContent({ + required this.replied, + required this.inReplyTo, + }); + + DSReplyContent.fromJson(Map json) + : replied = DSReplyContentReplied.fromJson( + json['replied'], + ), + inReplyTo = DSReplyContentInReplyTo.fromJson( + json['inReplyTo'], + ); +} diff --git a/lib/src/models/ds_reply_content_in_reply_to.model.dart b/lib/src/models/ds_reply_content_in_reply_to.model.dart new file mode 100644 index 00000000..cb213675 --- /dev/null +++ b/lib/src/models/ds_reply_content_in_reply_to.model.dart @@ -0,0 +1,19 @@ +class DSReplyContentInReplyTo { + String id; + String type; + dynamic value; + String direction; + + DSReplyContentInReplyTo({ + required this.id, + required this.type, + required this.value, + required this.direction, + }); + + DSReplyContentInReplyTo.fromJson(Map json) + : id = json['id'], + type = json['type'], + value = json['value'], + direction = json['direction']; +} diff --git a/lib/src/models/ds_reply_content_replied.model.dart b/lib/src/models/ds_reply_content_replied.model.dart new file mode 100644 index 00000000..6b2d2bcf --- /dev/null +++ b/lib/src/models/ds_reply_content_replied.model.dart @@ -0,0 +1,13 @@ +class DSReplyContentReplied { + String type; + dynamic value; + + DSReplyContentReplied({ + required this.type, + required this.value, + }); + + DSReplyContentReplied.fromJson(Map json) + : type = json['type'], + value = json['value']; +} diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index c4eeb2ef..0809f1e0 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -102,6 +102,9 @@ abstract class DSFileService { return null; } - static String getFileExtensionFromMime(String? mimeType) => - extensionFromMime(mimeType ?? ''); + static String getFileExtensionFromMime(String? mimeType) { + return mimeType == 'application/vnd.ms-powerpoint' + ? 'ppt' + : extensionFromMime(mimeType ?? ''); + } } diff --git a/lib/src/themes/colors/ds_colors.theme.dart b/lib/src/themes/colors/ds_colors.theme.dart index a0dfda1a..b29d0495 100644 --- a/lib/src/themes/colors/ds_colors.theme.dart +++ b/lib/src/themes/colors/ds_colors.theme.dart @@ -52,6 +52,7 @@ abstract class DSColors { static const Color surface1 = Color(0xFFF6F6F6); static const Color surface3 = Color(0xFFC7C7C7); static const Color contentDefault = Color(0xFF454545); + static const Color contentGhost = Color(0xFF949494); static const Color disabledText = Color(0xFF637798); static const Color disabledBg = Color(0xFFE8F2FF); diff --git a/lib/src/utils/ds_utils.util.dart b/lib/src/utils/ds_utils.util.dart index 626f51e8..b0ef6d92 100644 --- a/lib/src/utils/ds_utils.util.dart +++ b/lib/src/utils/ds_utils.util.dart @@ -10,6 +10,7 @@ abstract class DSUtils { static const bubbleMinSize = 240.0; static const bubbleMaxSize = 480.0; static const defaultAnimationDuration = Duration(milliseconds: 300); + static bool shouldShowReplyContainer = false; static String generateUniqueID() => md5.convert(utf8.encode(DateTime.now().toIso8601String())).toString(); 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_message_bubble.widget.dart b/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart index ae9f308b..7c16bdba 100644 --- a/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../enums/ds_align.enum.dart'; import '../../../enums/ds_border_radius.enum.dart'; import '../../../models/ds_message_bubble_style.model.dart'; +import '../../../models/ds_reply_content.model.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../ds_message_bubble.widget.dart'; import 'ds_audio_player.widget.dart'; @@ -14,12 +15,14 @@ class DSAudioMessageBubble extends StatelessWidget { final DSMessageBubbleStyle style; final String? uniqueId; final bool shouldAuthenticate; + final DSReplyContent? replyContent; DSAudioMessageBubble({ super.key, required this.uri, required this.align, this.uniqueId, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, final DSMessageBubbleStyle? style, @@ -33,44 +36,43 @@ class DSAudioMessageBubble extends StatelessWidget { return DSMessageBubble( borderRadius: borderRadius, align: align, + replyContent: replyContent, style: style, - padding: const EdgeInsets.only( - left: 4.0, - right: 8.0, - top: 8.0, - bottom: 8.0, - ), - child: DSAudioPlayer( - uri: uri, - shouldAuthenticate: shouldAuthenticate, - controlForegroundColor: isLightBubbleBackground - ? DSColors.neutralDarkRooftop - : DSColors.neutralLightSnow, - labelColor: isLightBubbleBackground - ? DSColors.neutralDarkCity - : DSColors.neutralLightSnow, - bufferActiveTrackColor: isLightBubbleBackground - ? DSColors.neutralMediumWave - : DSColors.neutralMediumElephant, - bufferInactiveTrackColor: isLightBubbleBackground - ? DSColors.neutralDarkRooftop - : DSColors.neutralLightBox, - sliderActiveTrackColor: isLightBubbleBackground - ? DSColors.primaryNight - : DSColors.primaryLight, - sliderThumbColor: isLightBubbleBackground - ? DSColors.neutralDarkRooftop - : DSColors.neutralLightSnow, - speedForegroundColor: isLightBubbleBackground - ? DSColors.neutralDarkCity - : DSColors.neutralLightSnow, - speedBorderColor: isLightBubbleBackground - ? isDefaultBubbleColors - ? DSColors.neutralMediumSilver - : DSColors.neutralDarkCity - : isDefaultBubbleColors - ? DSColors.disabledText - : DSColors.neutralLightSnow, + padding: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.fromLTRB(4.0, 8.0, 8.0, 8.0), + child: DSAudioPlayer( + uri: uri, + shouldAuthenticate: shouldAuthenticate, + controlForegroundColor: isLightBubbleBackground + ? DSColors.neutralDarkRooftop + : DSColors.neutralLightSnow, + labelColor: isLightBubbleBackground + ? DSColors.neutralDarkCity + : DSColors.neutralLightSnow, + bufferActiveTrackColor: isLightBubbleBackground + ? DSColors.neutralMediumWave + : DSColors.neutralMediumElephant, + bufferInactiveTrackColor: isLightBubbleBackground + ? DSColors.neutralDarkRooftop + : DSColors.neutralLightBox, + sliderActiveTrackColor: isLightBubbleBackground + ? DSColors.primaryNight + : DSColors.primaryLight, + sliderThumbColor: isLightBubbleBackground + ? DSColors.neutralDarkRooftop + : DSColors.neutralLightSnow, + speedForegroundColor: isLightBubbleBackground + ? DSColors.neutralDarkCity + : DSColors.neutralLightSnow, + speedBorderColor: isLightBubbleBackground + ? isDefaultBubbleColors + ? DSColors.neutralMediumSilver + : DSColors.neutralDarkCity + : isDefaultBubbleColors + ? DSColors.disabledText + : DSColors.neutralLightSnow, + ), ), ); } 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_contact_message_bubble.widget.dart b/lib/src/widgets/chat/ds_contact_message_bubble.widget.dart index 9d9f73fb..abebb950 100644 --- a/lib/src/widgets/chat/ds_contact_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_contact_message_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../../themes/texts/utils/ds_font_weights.theme.dart'; import '../texts/ds_body_text.widget.dart'; @@ -14,6 +15,7 @@ class DSContactMessageBubble extends StatelessWidget { final String? phone; final String? email; final String? address; + final DSReplyContent? replyContent; final DSAlign align; final List borderRadius; final DSMessageBubbleStyle style; @@ -25,6 +27,7 @@ class DSContactMessageBubble extends StatelessWidget { required this.address, required this.email, required this.align, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], DSMessageBubbleStyle? style, }) : style = style ?? DSMessageBubbleStyle(); @@ -34,9 +37,10 @@ class DSContactMessageBubble extends StatelessWidget { return DSMessageBubble( align: align, borderRadius: borderRadius, - padding: const EdgeInsets.all(16.0), + padding: EdgeInsets.zero, shouldUseDefaultSize: true, style: style, + replyContent: replyContent, child: _buildContactCard(), ); } @@ -45,44 +49,51 @@ class DSContactMessageBubble extends StatelessWidget { final foregroundColor = style.isLightBubbleBackground(align) ? DSColors.neutralDarkCity : DSColors.neutralLightSnow; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Visibility( - visible: name != null, - child: DSBodyText( - name, - fontWeight: DSFontWeights.semiBold, - color: foregroundColor, - overflow: TextOverflow.visible, - ), - ), - const SizedBox(height: 16.0), - /// TODO(format): Format phone number - if (phone != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: _buildContactField( - title: 'Telefone', - body: phone!, - foregroundColor: foregroundColor), + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: name != null, + child: DSBodyText( + name, + fontWeight: DSFontWeights.semiBold, + color: foregroundColor, + overflow: TextOverflow.visible, + ), ), - if (email != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: _buildContactField( - title: 'E-mail', - body: email!, - foregroundColor: foregroundColor), - ), - if (address != null) - _buildContactField( - title: 'Endereço', - body: address!, - foregroundColor: foregroundColor, - ), - ], + const SizedBox(height: 16.0), + + /// TODO(format): Format phone number + if (phone != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: _buildContactField( + title: 'Telefone', + body: phone!, + foregroundColor: foregroundColor), + ), + if (email != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: _buildContactField( + title: 'E-mail', + body: email!, + foregroundColor: foregroundColor), + ), + if (address != null) + _buildContactField( + title: 'Endereço', + body: address!, + foregroundColor: foregroundColor, + ), + ], + ), ); } 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 625ed975..1e5e046d 100644 --- a/lib/src/widgets/chat/ds_file_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_file_message_bubble.widget.dart @@ -5,9 +5,11 @@ import '../../controllers/chat/ds_file_message_bubble.controller.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.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 +24,8 @@ class DSFileMessageBubble extends StatelessWidget { final List borderRadius; final DSMessageBubbleStyle style; final bool shouldAuthenticate; + final bool isUploading; + final DSReplyContent? replyContent; /// Creates a Design System's [DSMessageBubble] used on files other than image, audio, or video DSFileMessageBubble({ @@ -33,29 +37,38 @@ class DSFileMessageBubble extends StatelessWidget { 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, - 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(), + ], + ), ), ), ), @@ -71,15 +84,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, ), - ], - ), ), ); } 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 260c9ecd..1f7bf9b3 100644 --- a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart @@ -6,7 +6,9 @@ import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_document_select.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.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'; @@ -32,8 +34,8 @@ class DSImageMessageBubble extends StatefulWidget { this.onOpenLink, this.shouldAuthenticate = false, this.mediaType, - this.imageMaxHeight, - this.imageMinHeight, + this.isUploading = false, + this.replyContent, }) : style = style ?? DSMessageBubbleStyle(); final DSAlign align; @@ -51,8 +53,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 DSReplyContent? replyContent; @override State createState() => _DSImageMessageBubbleState(); @@ -82,86 +84,88 @@ class _DSImageMessageBubbleState extends State : DSColors.neutralLightSnow; return DSMessageBubble( - defaultMaxSize: 360.0, + replyContent: widget.replyContent, + defaultMaxSize: DSUtils.bubbleMinSize, shouldUseDefaultSize: true, align: widget.align, borderRadius: widget.borderRadius, padding: EdgeInsets.zero, hasSpacer: widget.hasSpacer, style: widget.style, - child: LayoutBuilder( - builder: (_, constraints) { - 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, - ), - ), - if ((widget.title?.isNotEmpty ?? false) || - (widget.text?.isNotEmpty ?? false)) + child: Padding( + padding: widget.replyContent == null + ? EdgeInsets.zero + : const EdgeInsets.only(top: 8.0), + child: LayoutBuilder( + builder: (_, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ SizedBox( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.title?.isNotEmpty ?? false) - DSCaptionText( - widget.title!, - color: foregroundColor, - isSelectable: true, - ), - if ((widget.text?.isNotEmpty ?? false) && - (widget.title?.isNotEmpty ?? false)) - const SizedBox( - height: 6.0, - ), - if (widget.text?.isNotEmpty ?? false) - DSShowMoreText( - text: widget.text!, - maxWidth: constraints.maxWidth, + 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.showSelect) - DSDocumentSelect( - align: widget.align, - options: widget.selectOptions, - onSelected: widget.onSelected, - onOpenLink: widget.onOpenLink, - style: widget.style, - ), - ], - ); - }, + if ((widget.title?.isNotEmpty ?? false) || + (widget.text?.isNotEmpty ?? false)) + SizedBox( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title?.isNotEmpty ?? false) + DSCaptionText( + widget.title!, + color: foregroundColor, + isSelectable: true, + ), + if ((widget.text?.isNotEmpty ?? false) && + (widget.title?.isNotEmpty ?? false)) + const SizedBox( + height: 6.0, + ), + if (widget.text?.isNotEmpty ?? false) + DSShowMoreText( + text: widget.text!, + maxWidth: constraints.maxWidth, + align: widget.align, + style: widget.style, + ) + ], + ), + ), + ), + if (widget.showSelect) + DSDocumentSelect( + align: widget.align, + options: widget.selectOptions, + onSelected: widget.onSelected, + onOpenLink: widget.onOpenLink, + style: widget.style, + ), + ], + ); + }, + ), ), ); } 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 a838ca8c..b4616fcf 100644 --- a/lib/src/widgets/chat/ds_location_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_location_message_bubble.widget.dart @@ -4,9 +4,11 @@ import 'package:map_launcher/map_launcher.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.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'; @@ -16,6 +18,7 @@ class DSLocationMessageBubble extends StatelessWidget { final DSAlign align; final DSMessageBubbleStyle style; final String? title; + final DSReplyContent? replyContent; final String latitude; final String longitude; final List borderRadius; @@ -25,6 +28,7 @@ class DSLocationMessageBubble extends StatelessWidget { required this.align, required this.latitude, required this.longitude, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], DSMessageBubbleStyle? style, this.title, @@ -53,52 +57,58 @@ 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, align: align, style: style, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - hasValidCoordinates - ? DSCachedNetworkImageView( - url: - 'https://maps.googleapis.com/maps/api/staticmap?&size=360x360&markers=$latitude,$longitude&key=${DSAuthService.googleKey}', - placeholder: (_, __) => _buildLoading(), - align: align, - style: style, - ) - : SizedBox( - width: 240, - height: 240, - child: Icon( - DSIcons.file_image_broken_outline, - size: 80, - color: style.isLightBubbleBackground(align) - ? DSColors.neutralMediumElephant - : DSColors.neutralMediumCloud, + child: Padding( + padding: replyContent == null + ? EdgeInsets.zero + : const EdgeInsets.only(top: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + hasValidCoordinates + ? DSCachedNetworkImageView( + url: + 'https://maps.googleapis.com/maps/api/staticmap?&size=360x360&markers=$latitude,$longitude&key=${DSAuthService.googleKey}', + placeholder: (_, __) => _buildLoading(), + align: align, + style: style, + ) + : SizedBox( + width: 240, + height: 240, + child: Icon( + DSIcons.file_image_broken_outline, + size: 80, + color: style.isLightBubbleBackground(align) + ? DSColors.neutralMediumElephant + : DSColors.neutralMediumCloud, + ), ), + if (title?.isNotEmpty ?? false) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - if (title?.isNotEmpty ?? false) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Align( - alignment: Alignment.topLeft, - child: DSBodyText( - title!, - color: foregroundColor, - isSelectable: true, - overflow: TextOverflow.visible, + child: Align( + alignment: Alignment.topLeft, + child: DSBodyText( + title!, + color: foregroundColor, + isSelectable: true, + overflow: TextOverflow.visible, + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/src/widgets/chat/ds_message_bubble.widget.dart b/lib/src/widgets/chat/ds_message_bubble.widget.dart index cb1a62c5..afa3bb07 100644 --- a/lib/src/widgets/chat/ds_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_message_bubble.widget.dart @@ -3,13 +3,16 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../utils/ds_bubble.util.dart'; import '../../utils/ds_utils.util.dart'; import '../animations/ds_animated_size.widget.dart'; +import 'ds_reply_container.widget.dart'; class DSMessageBubble extends StatelessWidget { final DSAlign align; final Widget child; + final DSReplyContent? replyContent; final List borderRadius; final EdgeInsets padding; final bool shouldUseDefaultSize; @@ -22,6 +25,7 @@ class DSMessageBubble extends StatelessWidget { Key? key, required this.align, required this.child, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], this.padding = const EdgeInsets.symmetric( vertical: 8.0, @@ -34,12 +38,21 @@ class DSMessageBubble extends StatelessWidget { this.hasSpacer = true, }) : super(key: key); - BorderRadius _getBorderRadius() { - return borderRadius.getCircularBorderRadius( - maxRadius: 22.0, - minRadius: 2.0, - ); - } + @override + Widget build(BuildContext context) => Column( + children: [ + Row( + mainAxisAlignment: align == DSAlign.right + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: DSBubbleUtils.addSpacer( + align: align, + hasSpacer: hasSpacer, + child: _messageContainer(), + ), + ), + ], + ); Widget _messageContainer() { return DSAnimatedSize( @@ -60,26 +73,29 @@ class DSMessageBubble extends StatelessWidget { : null, padding: padding, color: style.bubbleBackgroundColor(align), - child: child, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (replyContent != null) + DSReplyContainer( + replyContent: replyContent!, + style: style, + align: align, + ), + child, + ], + ), ), ), ), ); } - @override - Widget build(BuildContext context) => Column( - children: [ - Row( - mainAxisAlignment: align == DSAlign.right - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: DSBubbleUtils.addSpacer( - align: align, - hasSpacer: hasSpacer, - child: _messageContainer(), - ), - ), - ], - ); + BorderRadius _getBorderRadius() { + return borderRadius.getCircularBorderRadius( + maxRadius: 22.0, + minRadius: 2.0, + ); + } } diff --git a/lib/src/widgets/chat/ds_reply_container.widget.dart b/lib/src/widgets/chat/ds_reply_container.widget.dart new file mode 100644 index 00000000..67332dcc --- /dev/null +++ b/lib/src/widgets/chat/ds_reply_container.widget.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; + +import '../../enums/ds_align.enum.dart'; +import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; +import '../../themes/colors/ds_colors.theme.dart'; +import '../../themes/icons/ds_icons.dart'; +import '../../utils/ds_message_content_type.util.dart'; +import '../texts/ds_body_text.widget.dart'; +import '../texts/ds_caption_text.widget.dart'; + +class DSReplyContainer extends StatelessWidget { + DSReplyContainer({ + super.key, + required this.replyContent, + required this.align, + DSMessageBubbleStyle? style, + }) : style = style ?? DSMessageBubbleStyle(); + + final DSAlign align; + final DSReplyContent replyContent; + final DSMessageBubbleStyle style; + + Color get _foregroundColor => style.isLightBubbleBackground(align) + ? const Color.fromARGB(255, 39, 4, 4) + : DSColors.surface1; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + _buildTitle(), + _buildReplyContainer(), + ], + ), + ); + } + + Widget _buildTitle() => Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + DSIcons.undo_outline, + color: style.isLightBubbleBackground(align) + ? DSColors.neutralDarkCity + : DSColors.neutralLightSnow, + size: 24.0, + ), + const SizedBox(width: 8.0), + DSCaptionText( + 'Reply', + fontStyle: FontStyle.italic, + color: style.isLightBubbleBackground(align) + ? DSColors.neutralDarkCity + : DSColors.neutralLightSnow, + ), + ], + ), + ); + + Widget _buildReplyContainer() => DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + border: Border.all( + color: style.isLightBubbleBackground(align) + ? DSColors.contentGhost + : DSColors.contentDisable, + ), + color: style.isLightBubbleBackground(align) + ? DSColors.surface3 + : DSColors.contentDefault, + ), + child: IntrinsicHeight( + child: Row( + children: [ + Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + ), + ), + color: DSColors.primary, + ), + width: 4.0, + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: _buildReply(), + ), + ), + ], + ), + ), + ); + + Widget _buildReply() => switch (replyContent.inReplyTo.type) { + DSMessageContentType.textPlain => _buildTextPlain(), + DSMessageContentType.applicationJson => _buildApplicationJson(), + DSMessageContentType.select => _buidSelect(), + _ => _buildDefault(), + }; + + Widget _buidSelect() => DSBodyText( + replyContent.inReplyTo.value['text'], + color: _foregroundColor, + overflow: TextOverflow.visible, + ); + + Widget _buildApplicationJson() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (replyContent.inReplyTo.value['interactive']['body']?['text'] != + null) + DSBodyText( + replyContent.inReplyTo.value['interactive']['body']?['text'], + color: _foregroundColor, + overflow: TextOverflow.visible, + ), + if (replyContent.inReplyTo.value['interactive']['footer']?['text'] != + null) + DSCaptionText( + replyContent.inReplyTo.value['interactive']['footer']?['text'], + fontStyle: FontStyle.italic, + overflow: TextOverflow.visible, + color: _foregroundColor, + ), + ], + ); + + Widget _buildTextPlain() => replyContent.inReplyTo.value is String + ? DSBodyText( + replyContent.inReplyTo.value, + color: _foregroundColor, + overflow: TextOverflow.visible, + ) + : _buildDefault(); + + Widget _buildDefault() => Row( + children: [ + Icon( + DSIcons.warning_outline, + color: _foregroundColor, + size: 24.0, + ), + const SizedBox(width: 8.0), + Flexible( + child: DSBodyText( + 'Failed to load message', + overflow: TextOverflow.visible, + color: _foregroundColor, + ), + ), + ], + ); +} diff --git a/lib/src/widgets/chat/ds_request_location_bubble.widget.dart b/lib/src/widgets/chat/ds_request_location_bubble.widget.dart index 511b7923..c170bc60 100644 --- a/lib/src/widgets/chat/ds_request_location_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_request_location_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../utils/ds_bubble.util.dart'; import '../../utils/ds_message_content_type.util.dart'; import '../buttons/ds_request_location_button.widget.dart'; @@ -14,6 +15,7 @@ class DSRequestLocationBubble extends StatelessWidget { required this.label, required this.value, required this.align, + this.replyContent, this.type = DSMessageContentType.textPlain, this.borderRadius = const [DSBorderRadius.all], this.showRequestLocationButton = false, @@ -23,6 +25,7 @@ class DSRequestLocationBubble extends StatelessWidget { final String? label; final String type; final String? value; + final DSReplyContent? replyContent; final DSAlign align; final List borderRadius; final bool showRequestLocationButton; @@ -45,6 +48,7 @@ class DSRequestLocationBubble extends StatelessWidget { ), child: DSTextMessageBubble( text: value!, + replyContent: replyContent, align: align, borderRadius: borderRadius, style: style, diff --git a/lib/src/widgets/chat/ds_text_message_bubble.widget.dart b/lib/src/widgets/chat/ds_text_message_bubble.widget.dart index b82d4a50..fc81bc89 100644 --- a/lib/src/widgets/chat/ds_text_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_text_message_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../../utils/ds_linkify.util.dart'; import 'ds_message_bubble.widget.dart'; @@ -16,6 +17,7 @@ class DSTextMessageBubble extends StatefulWidget { final bool hasSpacer; final List borderRadius; final dynamic selectContent; + final DSReplyContent? replyContent; final bool showSelect; final void Function(String, Map)? onSelected; final DSMessageBubbleStyle style; @@ -24,6 +26,7 @@ class DSTextMessageBubble extends StatefulWidget { Key? key, required this.text, required this.align, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], this.selectContent, this.hasSpacer = true, @@ -61,6 +64,7 @@ class _DSTextMessageBubbleState extends State { padding: EdgeInsets.zero, style: widget.style, hasSpacer: widget.hasSpacer, + replyContent: widget.replyContent, child: _buildText(), ); } diff --git a/lib/src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart b/lib/src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart index 34f16cc6..bb7732b9 100644 --- a/lib/src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../../themes/icons/ds_icons.dart'; import '../texts/ds_body_text.widget.dart'; @@ -11,6 +12,7 @@ import 'ds_message_bubble.widget.dart'; class DSUnsupportedContentMessageBubble extends StatelessWidget { final DSAlign align; final Widget? leftWidget; + final DSReplyContent? replyContent; final String? text; final TextOverflow overflow; final List borderRadius; @@ -21,6 +23,7 @@ class DSUnsupportedContentMessageBubble extends StatelessWidget { required this.align, this.leftWidget, this.text, + this.replyContent, this.overflow = TextOverflow.ellipsis, this.borderRadius = const [DSBorderRadius.all], DSMessageBubbleStyle? style, @@ -37,6 +40,7 @@ class DSUnsupportedContentMessageBubble extends StatelessWidget { borderRadius: borderRadius, align: align, style: style, + replyContent: replyContent, child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/widgets/chat/ds_weblink.widget.dart b/lib/src/widgets/chat/ds_weblink_message_bubble.widget.dart similarity index 90% rename from lib/src/widgets/chat/ds_weblink.widget.dart rename to lib/src/widgets/chat/ds_weblink_message_bubble.widget.dart index 0bd3689f..c67f3e34 100644 --- a/lib/src/widgets/chat/ds_weblink.widget.dart +++ b/lib/src/widgets/chat/ds_weblink_message_bubble.widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../texts/ds_body_text.widget.dart'; import '../texts/ds_headline_small_text.widget.dart'; @@ -10,7 +11,7 @@ import 'ds_message_bubble.widget.dart'; /// A Design System widget used to display weblinks. -class DSWeblink extends StatelessWidget { +class DSWeblinkMessageBubble extends StatelessWidget { /// Show the [title] on the card final String title; @@ -29,12 +30,16 @@ class DSWeblink extends StatelessWidget { /// Card styling to adjust custom colors final DSMessageBubbleStyle style; - DSWeblink({ + /// replyContent + final DSReplyContent? replyContent; + + DSWeblinkMessageBubble({ Key? key, required this.title, required this.text, required this.align, required this.url, + this.replyContent, this.borderRadius = const [DSBorderRadius.all], DSMessageBubbleStyle? style, }) : style = style ?? DSMessageBubbleStyle(), @@ -50,6 +55,7 @@ class DSWeblink extends StatelessWidget { return DSMessageBubble( align: align, + replyContent: replyContent, borderRadius: borderRadius, style: style, child: Column( 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 204850c0..d8bed8c7 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 @@ -7,9 +7,12 @@ import '../../../controllers/chat/ds_video_message_bubble.controller.dart'; import '../../../enums/ds_align.enum.dart'; import '../../../enums/ds_border_radius.enum.dart'; import '../../../models/ds_message_bubble_style.model.dart'; +import '../../../models/ds_reply_content.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'; @@ -48,6 +51,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 DSReplyContent? 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. @@ -65,6 +74,8 @@ class DSVideoMessageBubble extends StatefulWidget { this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, DSMessageBubbleStyle? style, + this.isUploading = false, + this.replyContent, }) : style = style ?? DSMessageBubbleStyle(); @override @@ -74,16 +85,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 @@ -112,6 +129,9 @@ class _DSVideoMessageBubbleState extends State : DSColors.neutralDarkCity; return DSMessageBubble( + defaultMaxSize: DSUtils.bubbleMinSize, + shouldUseDefaultSize: true, + replyContent: widget.replyContent, align: widget.align, borderRadius: widget.borderRadius, padding: EdgeInsets.zero, @@ -120,69 +140,106 @@ class _DSVideoMessageBubbleState extends State builder: (_, constraints) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx( - () => SizedBox( - height: 240, - width: 240, - child: _controller.hasError.value - ? const Icon( - DSIcons.video_broken_outline, - size: 80.0, - color: DSColors.neutralDarkRooftop, - ) - : _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, - ), - ), - ), - ), + 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, + ), + ), + ), + ) + : _buidErrorIcon(), ), if (widget.text?.isNotEmpty ?? false) - 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, + SizedBox( + width: DSUtils.bubbleMinSize, + 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, + ), ), ), ], @@ -190,4 +247,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 394672c7..eec23aa5 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'; @@ -8,8 +10,10 @@ import '../../models/ds_document_select.model.dart'; import '../../models/ds_media_link.model.dart'; import '../../models/ds_message_bubble_avatar_config.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; +import '../../models/ds_reply_content.model.dart'; import '../../services/ds_file.service.dart'; import '../../utils/ds_message_content_type.util.dart'; +import '../../utils/ds_utils.util.dart'; import '../chat/audio/ds_audio_message_bubble.widget.dart'; import '../chat/ds_application_json_message_bubble.widget.dart'; import '../chat/ds_carrousel.widget.dart'; @@ -21,7 +25,7 @@ import '../chat/ds_quick_reply.widget.dart'; import '../chat/ds_request_location_bubble.widget.dart'; import '../chat/ds_text_message_bubble.widget.dart'; import '../chat/ds_unsupported_content_message_bubble.widget.dart'; -import '../chat/ds_weblink.widget.dart'; +import '../chat/ds_weblink_message_bubble.widget.dart'; import '../chat/video/ds_video_message_bubble.widget.dart'; import '../ticket_message/ds_ticket_message.widget.dart'; @@ -43,6 +47,8 @@ class DSCard extends StatelessWidget { this.customer, this.showQuickReplyOptions = false, this.showRequestLocationButton = false, + this.replyContent, + this.isUploading = false, }) : style = style ?? DSMessageBubbleStyle(); final String type; @@ -58,6 +64,8 @@ class DSCard extends StatelessWidget { final Map? customer; final bool showQuickReplyOptions; final bool showRequestLocationButton; + final DSReplyContent? replyContent; + final bool isUploading; @override Widget build(BuildContext context) { @@ -73,15 +81,18 @@ class DSCard extends StatelessWidget { align: align, borderRadius: borderRadius, style: style, + replyContent: replyContent, ); case DSMessageContentType.contact: return _buildContact(); case DSMessageContentType.reply: + final replyContent = DSReplyContent.fromJson(content); + return DSCard( - type: content['replied']['type'], - content: content['replied']['value'], + type: replyContent.replied.type, + content: replyContent.replied.value, align: align, borderRadius: borderRadius, status: status, @@ -91,6 +102,7 @@ class DSCard extends StatelessWidget { onOpenLink: onOpenLink, messageId: messageId, customer: customer, + replyContent: DSUtils.shouldShowReplyContainer ? replyContent : null, ); case DSMessageContentType.mediaLink: @@ -114,11 +126,12 @@ class DSCard extends StatelessWidget { ); case DSMessageContentType.webLink: - return DSWeblink( + return DSWeblinkMessageBubble( title: content['title'], text: content['text'], url: content['uri'], align: align, + replyContent: replyContent, borderRadius: borderRadius, style: style, ); @@ -131,6 +144,7 @@ class DSCard extends StatelessWidget { borderRadius: borderRadius, align: align, style: style, + replyContent: replyContent, ); case DSMessageContentType.ticket: return DSTicketMessage( @@ -155,6 +169,7 @@ class DSCard extends StatelessWidget { default: return DSUnsupportedContentMessageBubble( align: align, + replyContent: replyContent, borderRadius: borderRadius, style: style, ); @@ -179,6 +194,7 @@ class DSCard extends StatelessWidget { selectOptions: documentSelectModel.options, borderRadius: borderRadius, style: style, + replyContent: replyContent, showSelect: true, onSelected: onSelected, onOpenLink: onOpenLink, @@ -197,6 +213,7 @@ class DSCard extends StatelessWidget { child: DSTextMessageBubble( align: align, text: content['text'], + replyContent: replyContent, borderRadius: borderRadius, style: style, ), @@ -218,6 +235,7 @@ class DSCard extends StatelessWidget { borderRadius: borderRadius, selectContent: content, showSelect: true, + replyContent: replyContent, onSelected: onSelected, style: style, ); @@ -230,6 +248,7 @@ class DSCard extends StatelessWidget { address: content['address'], email: content['email'], align: align, + replyContent: replyContent, style: style, borderRadius: borderRadius, ); @@ -251,10 +270,13 @@ 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, + replyContent: replyContent, uniqueId: messageId, shouldAuthenticate: shouldAuthenticate, ); @@ -271,11 +293,12 @@ class DSCard extends StatelessWidget { : avatarConfig.sentAvatar, text: media.text, title: media.title, + replyContent: replyContent, borderRadius: borderRadius, style: style, shouldAuthenticate: shouldAuthenticate, mediaType: media.type, - imageMaxHeight: 300.0, + isUploading: isUploading, ); } else if (media.type.contains('video')) { return DSVideoMessageBubble( @@ -289,21 +312,25 @@ class DSCard extends StatelessWidget { ? avatarConfig.receivedAvatar : avatarConfig.sentAvatar, text: media.text, + replyContent: replyContent, borderRadius: borderRadius, style: style, mediaSize: size, shouldAuthenticate: shouldAuthenticate, + isUploading: isUploading, ); } else { return DSFileMessageBubble( align: align, url: media.uri, + replyContent: replyContent, size: size, filename: media.title ?? '${media.uri.hashCode}.${DSFileService.getFileExtensionFromMime(media.type)}', borderRadius: borderRadius, style: style, shouldAuthenticate: shouldAuthenticate, + isUploading: isUploading, ); } } @@ -315,6 +342,7 @@ class DSCard extends StatelessWidget { return DSRequestLocationBubble( label: 'Send location', + replyContent: replyContent, type: type, value: value, align: align, 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 25946c0f..012e738b 100644 --- a/lib/src/widgets/utils/ds_group_card.widget.dart +++ b/lib/src/widgets/utils/ds_group_card.widget.dart @@ -211,6 +211,7 @@ class _DSGroupCardState extends State { }; } } + groups.add(group); return groups; } @@ -260,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/pubspec.yaml b/pubspec.yaml index 0b36c416..4f4cb247 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blip_ds description: Blip Design System for Flutter. -version: 0.0.89 +version: 0.0.90 homepage: https://github.com/takenet/blip-ds-flutter#readme repository: https://github.com/takenet/blip-ds-flutter diff --git a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart index c2f92c86..07d10eb7 100644 --- a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart +++ b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart @@ -53,6 +53,57 @@ class SampleMessageBubbleShowcase extends StatelessWidget { return Obx( () => Column( children: [ + DSTextMessageBubble( + text: 'Essa foto é linda', + align: DSAlign.left, + replyContent: DSReplyContent( + replied: DSReplyContentReplied( + type: 'text/plain', + value: 'Essa foto é linda', + ), + inReplyTo: DSReplyContentInReplyTo( + id: 'id', + type: 'application/vnd.lime.media-link+json', + value: + '{uri:${_sampleImages['extraLarge']!}, type: "image/jpeg"}', + direction: 'received', + ), + ), + ), + DSTextMessageBubble( + text: 'Sim', + align: DSAlign.left, + replyContent: DSReplyContent( + replied: DSReplyContentReplied( + type: 'text/plain', + value: 'Sim', + ), + inReplyTo: DSReplyContentInReplyTo( + id: 'id', + type: 'text/plain', + value: 'Você gostaria de um atendimento humano?', + direction: 'received', + ), + ), + ), + DSImageMessageBubble( + align: DSAlign.left, + url: _sampleImages['extraLarge']!, + appBarText: 'appBarText', + replyContent: DSReplyContent( + replied: DSReplyContentReplied( + type: 'text/plain', + value: + '{type: "image/jpeg", uri: ${_sampleImages['extraLarge']!}}', + ), + inReplyTo: DSReplyContentInReplyTo( + id: 'id', + type: 'text/plain', + value: 'Envie a imagem por favor', + direction: 'received', + ), + ), + ), DSApplicationJsonMessageBubble( align: DSAlign.right, borderRadius: const [ diff --git a/sample/lib/widgets/showcase/sample_weblink.showcase.dart b/sample/lib/widgets/showcase/sample_weblink.showcase.dart index 744cf769..e9a14517 100644 --- a/sample/lib/widgets/showcase/sample_weblink.showcase.dart +++ b/sample/lib/widgets/showcase/sample_weblink.showcase.dart @@ -8,14 +8,14 @@ class SampleWeblinkShowcase extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - DSWeblink( + DSWeblinkMessageBubble( title: 'Take Blip', text: 'Atenda de forma inteligente, no canal digital que seu cliente prefere', url: 'https://www.take.net/', align: DSAlign.right, ), - DSWeblink( + DSWeblinkMessageBubble( title: 'Take Blip', text: 'Atenda de forma inteligente, no canal digital que seu cliente prefere', 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