From 180c24ae72b98c9a95c4f29011ef2cae38223432 Mon Sep 17 00:00:00 2001 From: Ayush Shekhar Date: Wed, 13 Apr 2022 18:02:49 +0530 Subject: [PATCH] Implemented StreamAttachmentPackage and fixed operations in widgets linked to full screen implementation, also fixed tests --- analysis_options.yaml | 1 - .../lib/src/attachment/giphy_attachment.dart | 3 +- .../lib/src/attachment/image_attachment.dart | 4 +- .../lib/src/attachment/video_attachment.dart | 4 +- .../lib/src/attachment_actions_modal.dart | 155 +++++++----- .../lib/src/full_screen_media.dart | 118 +++++---- .../lib/src/gallery_footer.dart | 27 +-- .../lib/src/gallery_header.dart | 12 +- .../lib/src/image_group.dart | 3 +- .../lib/src/stream_attachment_package.dart | 18 ++ .../stream_chat_flutter/lib/src/utils.dart | 17 ++ .../lib/stream_chat_flutter.dart | 2 + packages/stream_chat_flutter/pubspec.yaml | 2 +- .../src/attachment_actions_modal_test.dart | 223 ++++++++++-------- .../test/src/full_screen_media_test.dart | 23 +- .../test/src/image_footer_test.dart | 2 +- .../src/theme/gallery_footer_theme_test.dart | 4 +- .../src/theme/gallery_header_theme_test.dart | 8 +- 18 files changed, 373 insertions(+), 253 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/stream_attachment_package.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 03a4220e..336d222e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -91,7 +91,6 @@ linter: - prefer_constructors_over_static_methods - prefer_contains - prefer_equal_for_default_values - - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 28e14e5e..6381c345 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -250,10 +250,9 @@ class StreamGiphyAttachment extends StreamAttachmentWidget { return StreamChannel( channel: channel, child: StreamFullScreenMedia( - mediaAttachments: message.attachments, + mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index b6de6bc1..bb4d8161 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -144,11 +144,11 @@ class StreamImageAttachment extends StreamAttachmentWidget { return StreamChannel( channel: channel, child: StreamFullScreenMedia( - mediaAttachments: message.attachments, + mediaAttachmentPackages: + message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index 541f1484..620ae66b 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -91,11 +91,11 @@ class StreamVideoAttachment extends StreamAttachmentWidget { builder: (_) => StreamChannel( channel: channel, child: StreamFullScreenMedia( - mediaAttachments: message.attachments, + mediaAttachmentPackages: + message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index a1b20450..3b5582df 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -9,14 +9,18 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; typedef AttachmentDownloader = Future Function( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }); +/// Callback to receive the path once the attachment asset is downloaded +typedef DownloadedPathCallback = void Function(String? path); + /// Widget that shows the options in the gallery view class AttachmentActionsModal extends StatelessWidget { /// Returns a new [AttachmentActionsModal] const AttachmentActionsModal({ Key? key, - required this.currentIndex, + required this.attachment, required this.message, this.onShowMessage, this.imageDownloader, @@ -28,12 +32,12 @@ class AttachmentActionsModal extends StatelessWidget { this.customActions = const [], }) : super(key: key); + /// The attachment object for which the actions are to be performed + final Attachment attachment; + /// The message containing the attachments final Message message; - /// Current page index - final int currentIndex; - /// Callback to show the message final VoidCallback? onShowMessage; @@ -62,7 +66,7 @@ class AttachmentActionsModal extends StatelessWidget { /// specified attributes overridden. AttachmentActionsModal copyWith({ Key? key, - int? currentIndex, + Attachment? attachment, Message? message, VoidCallback? onShowMessage, AttachmentDownloader? imageDownloader, @@ -75,7 +79,7 @@ class AttachmentActionsModal extends StatelessWidget { }) => AttachmentActionsModal( key: key ?? this.key, - currentIndex: currentIndex ?? this.currentIndex, + attachment: attachment ?? this.attachment, message: message ?? this.message, onShowMessage: onShowMessage ?? this.onShowMessage, imageDownloader: imageDownloader ?? this.imageDownloader, @@ -138,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget { if (showSave) _buildButton( context, - message.attachments[currentIndex].type == 'video' + attachment.type == 'video' ? context.translations.saveVideoLabel : context.translations.saveImageLabel, StreamSvgIcon.iconSave( @@ -146,15 +150,16 @@ class AttachmentActionsModal extends StatelessWidget { color: theme.colorTheme.textLowEmphasis, ), () { - final attachment = message.attachments[currentIndex]; final isImage = attachment.type == 'image'; final Future Function( Attachment, { void Function(int, int) progressCallback, + DownloadedPathCallback downloadedPathCallback, }) saveFile = fileDownloader ?? _downloadAttachment; final Future Function( Attachment, { void Function(int, int) progressCallback, + DownloadedPathCallback downloadedPathCallback, }) saveImage = imageDownloader ?? _downloadAttachment; final downloader = isImage ? saveImage : saveFile; @@ -162,15 +167,25 @@ class AttachmentActionsModal extends StatelessWidget { ValueNotifier<_DownloadProgress?>( _DownloadProgress.initial(), ); + final downloadedPathNotifier = ValueNotifier( + null, + ); downloader( attachment, progressCallback: (received, total) { + print('>>>>>>>>>>>>>>>'); + print('>>r : $received'); + print('>>t : $total'); + print('>>>>>>>>>>>>>>>'); progressNotifier.value = _DownloadProgress( total, received, ); }, + downloadedPathCallback: (String? path) { + downloadedPathNotifier.value = path; + }, ).catchError((e, stk) { progressNotifier.value = null; }); @@ -186,6 +201,7 @@ class AttachmentActionsModal extends StatelessWidget { builder: (context) => _buildDownloadProgressDialog( context, progressNotifier, + downloadedPathNotifier, ), ); }, @@ -204,8 +220,12 @@ class AttachmentActionsModal extends StatelessWidget { final channel = StreamChannel.of(context).channel; if (message.attachments.length > 1 || message.text?.isNotEmpty == true) { + final currentAttachmentIndex = + message.attachments.indexWhere( + (element) => element.id == attachment.id, + ); final remainingAttachments = [...message.attachments] - ..removeAt(currentIndex); + ..removeAt(currentAttachmentIndex); channel.updateMessage(message.copyWith( attachments: remainingAttachments, )); @@ -285,73 +305,86 @@ class AttachmentActionsModal extends StatelessWidget { Widget _buildDownloadProgressDialog( BuildContext context, ValueNotifier<_DownloadProgress?> progressNotifier, + ValueNotifier downloadedFilePathNotifier, ) { final theme = StreamChatTheme.of(context); return ValueListenableBuilder( - valueListenable: progressNotifier, - builder: (_, _DownloadProgress? progress, __) { - // Pop the dialog in case the progress is null or it's completed. - if (progress == null || progress.toProgressIndicatorValue == 1.0) { + valueListenable: downloadedFilePathNotifier, + builder: (_, String? path, __) { + final _downloadComplete = path != null && path.isNotEmpty; + // Pop the dialog in case the download has completed + if (_downloadComplete) { Future.delayed( const Duration(milliseconds: 500), () => Navigator.of(context).maybePop(), ); } - return Material( - type: MaterialType.transparency, - child: Center( - child: Container( - height: 182, - width: 182, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: theme.colorTheme.barsBg, - ), + return ValueListenableBuilder( + valueListenable: progressNotifier, + builder: (_, _DownloadProgress? progress, __) { + // Pop the dialog in case the progress is null. + if (progress == null) { + Future.delayed( + const Duration(milliseconds: 500), + () => Navigator.of(context).maybePop(), + ); + } + return Material( + type: MaterialType.transparency, child: Center( - child: progress == null - ? SizedBox( - height: 100, - width: 100, - child: StreamSvgIcon.error( - color: theme.colorTheme.disabled, - ), - ) - : progress.toProgressIndicatorValue == 1.0 + child: Container( + height: 182, + width: 182, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: theme.colorTheme.barsBg, + ), + child: Center( + child: progress == null ? SizedBox( - key: const Key('completedIcon'), - height: 160, - width: 160, - child: StreamSvgIcon.check( + height: 100, + width: 100, + child: StreamSvgIcon.error( color: theme.colorTheme.disabled, ), ) - : SizedBox( - height: 100, - width: 100, - child: Stack( - fit: StackFit.expand, - children: [ - CircularProgressIndicator( - value: progress.toProgressIndicatorValue, - strokeWidth: 8, - valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentPrimary, - ), + : _downloadComplete + ? SizedBox( + key: const Key('completedIcon'), + height: 160, + width: 160, + child: StreamSvgIcon.check( + color: theme.colorTheme.disabled, ), - Center( - child: Text( - '${progress.toPercentage}%', - style: theme.textTheme.headline.copyWith( - color: theme.colorTheme.textLowEmphasis, + ) + : SizedBox( + height: 100, + width: 100, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator( + strokeWidth: 8, + color: theme.colorTheme.accentPrimary, ), - ), + Center( + child: Text( + '${progress.receivedValueInMB} MB', + style: + theme.textTheme.headline.copyWith( + color: + theme.colorTheme.textLowEmphasis, + ), + ), + ), + ], ), - ], - ), - ), + ), + ), + ), ), - ), - ), + ); + }, ); }, ); @@ -360,6 +393,7 @@ class AttachmentActionsModal extends StatelessWidget { Future _downloadAttachment( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }) async { String? filePath; final appDocDir = await getTemporaryDirectory(); @@ -375,6 +409,7 @@ class AttachmentActionsModal extends StatelessWidget { onReceiveProgress: progressCallback, ); final result = await ImageGallerySaver.saveFile(filePath!); + downloadedPathCallback?.call((result as Map)['filePath']); return (result as Map)['filePath']; } } @@ -388,6 +423,8 @@ class _DownloadProgress { final int total; final int received; + String get receivedValueInMB => ((received / 1024) / 1024).toStringAsFixed(2); + double get toProgressIndicatorValue => received / total; int get toPercentage => (received * 100) ~/ total; diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 0ae3a475..37809b40 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:stream_chat_flutter/src/stream_attachment_package.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; @@ -32,8 +33,7 @@ class StreamFullScreenMedia extends StatefulWidget { /// Instantiate a new FullScreenImage const StreamFullScreenMedia({ Key? key, - required this.mediaAttachments, - required this.message, + required this.mediaAttachmentPackages, this.startIndex = 0, String? userName, this.onShowMessage, @@ -43,10 +43,7 @@ class StreamFullScreenMedia extends StatefulWidget { super(key: key); /// The url of the image - final List mediaAttachments; - - /// Message where attachments are attached - final Message message; + final List mediaAttachmentPackages; /// First index of media shown final int startIndex; @@ -100,8 +97,8 @@ class _StreamFullScreenMediaState extends State duration: const Duration(milliseconds: 300), ); _pageController = PageController(initialPage: widget.startIndex); - for (var i = 0; i < widget.mediaAttachments.length; i++) { - final attachment = widget.mediaAttachments[i]; + for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { + final attachment = widget.mediaAttachmentPackages[i].attachment; if (attachment.type != 'video') continue; final package = VideoPackage(attachment, showControls: true); videoPackages[attachment.id] = package; @@ -114,7 +111,8 @@ class _StreamFullScreenMediaState extends State return; } - final currentAttachment = widget.mediaAttachments[widget.startIndex]; + final currentAttachment = + widget.mediaAttachmentPackages[widget.startIndex].attachment; await Future.wait(videoPackages.values.map( (it) => it.initialize(), @@ -142,7 +140,8 @@ class _StreamFullScreenMediaState extends State return; } - final currentAttachment = widget.mediaAttachments[val]; + final currentAttachment = + widget.mediaAttachmentPackages[val].attachment; for (final e in videoPackages.values) { if (e._attachment != currentAttachment) { @@ -157,7 +156,9 @@ class _StreamFullScreenMediaState extends State } }, itemBuilder: (context, index) { - final attachment = widget.mediaAttachments[index]; + final currentAttachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = currentAttachmentPackage.attachment; if (attachment.type == 'image' || attachment.type == 'giphy') { final imageUrl = attachment.imageUrl ?? attachment.assetUrl ?? @@ -174,7 +175,7 @@ class _StreamFullScreenMediaState extends State maxScale: PhotoViewComputedScale.covered, minScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes( - tag: widget.mediaAttachments, + tag: widget.mediaAttachmentPackages, ), backgroundDecoration: BoxDecoration( color: ColorTween( @@ -218,53 +219,66 @@ class _StreamFullScreenMediaState extends State } return const SizedBox(); }, - itemCount: widget.mediaAttachments.length, + itemCount: widget.mediaAttachmentPackages.length, ), FadeTransition( opacity: _opacityAnimation, child: ValueListenableBuilder( valueListenable: _currentPage, - builder: (context, value, child) => Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamGalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: widget.message.createdAt, - time: widget.message.createdAt, - ), - onBackPressed: () { - Navigator.of(context).pop(); - }, - message: widget.message, - currentIndex: value, - onShowMessage: () { - widget.onShowMessage?.call( - widget.message, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (!widget.message.isEphemeral) - StreamGalleryFooter( - currentPage: value, - totalPages: widget.mediaAttachments.length, - mediaAttachments: widget.mediaAttachments, - message: widget.message, - mediaSelectedCallBack: (val) { - _currentPage.value = val; - _pageController.animateToPage( - val, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - Navigator.pop(context); + builder: (context, value, child) { + final _currentAttachmentPackage = + widget.mediaAttachmentPackages[value]; + final _currentMessage = _currentAttachmentPackage.message; + final _currentAttachment = + _currentAttachmentPackage.attachment; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamGalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: widget + .mediaAttachmentPackages[_currentPage.value] + .message + .createdAt, + time: widget + .mediaAttachmentPackages[_currentPage.value] + .message + .createdAt, + ), + onBackPressed: () { + Navigator.of(context).pop(); }, + message: _currentMessage, + attachment: _currentAttachment, + onShowMessage: () { + widget.onShowMessage?.call( + _currentMessage, + StreamChannel.of(context).channel, + ); + }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, ), - ], - ), + if (!_currentMessage.isEphemeral) + StreamGalleryFooter( + currentPage: value, + totalPages: widget.mediaAttachmentPackages.length, + mediaAttachmentPackages: + widget.mediaAttachmentPackages, + mediaSelectedCallBack: (val) { + _currentPage.value = val; + _pageController.animateToPage( + val, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + Navigator.pop(context); + }, + ), + ], + ); + }, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index a137281c..24cea1f2 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -18,16 +18,15 @@ typedef GalleryFooter = StreamGalleryFooter; /// {@endtemplate} class StreamGalleryFooter extends StatefulWidget implements PreferredSizeWidget { - /// Creates a channel header + /// Creates a StreamGalleryFooter const StreamGalleryFooter({ Key? key, - required this.message, this.onBackPressed, this.onTitleTap, this.onImageTap, this.currentPage = 0, this.totalPages = 0, - this.mediaAttachments = const [], + required this.mediaAttachmentPackages, this.mediaSelectedCallBack, this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), @@ -50,10 +49,7 @@ class StreamGalleryFooter extends StatefulWidget final int totalPages; /// All attachments to show - final List mediaAttachments; - - /// Message which attachments are attached to - final Message message; + final List mediaAttachmentPackages; /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; @@ -99,8 +95,8 @@ class _StreamGalleryFooterState extends State { color: galleryFooterThemeData.shareIconColor, ), onPressed: () async { - final attachment = - widget.mediaAttachments[widget.currentPage]; + final attachment = widget + .mediaAttachmentPackages[widget.currentPage].attachment; final url = attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl!; @@ -171,7 +167,7 @@ class _StreamGalleryFooterState extends State { builder: (context) { const crossAxisCount = 3; final noOfRowToShowInitially = - widget.mediaAttachments.length > crossAxisCount ? 2 : 1; + widget.mediaAttachmentPackages.length > crossAxisCount ? 2 : 1; final size = MediaQuery.of(context).size; final initialChildSize = 48 + (size.width * noOfRowToShowInitially) / crossAxisCount; @@ -212,7 +208,7 @@ class _StreamGalleryFooterState extends State { child: GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: widget.mediaAttachments.length, + itemCount: widget.mediaAttachmentPackages.length, padding: const EdgeInsets.all(1), // ignore: lines_longer_than_80_chars gridDelegate: @@ -223,7 +219,10 @@ class _StreamGalleryFooterState extends State { ), itemBuilder: (context, index) { Widget media; - final attachment = widget.mediaAttachments[index]; + final attachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = attachmentPackage.attachment; + final message = attachmentPackage.message; if (attachment.type == 'video') { media = InkWell( onTap: () => widget.mediaSelectedCallBack!(index), @@ -253,7 +252,7 @@ class _StreamGalleryFooterState extends State { return Stack( children: [ media, - if (widget.message.user != null) + if (message.user != null) Padding( padding: const EdgeInsets.all(8), child: Container( @@ -272,7 +271,7 @@ class _StreamGalleryFooterState extends State { ], ), child: StreamUserAvatar( - user: widget.message.user!, + user: message.user!, constraints: BoxConstraints.tight(const Size(24, 24)), showOnlineStatus: false, diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 9c8650b2..4f9b83dc 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -28,7 +28,7 @@ class StreamGalleryHeader extends StatelessWidget const StreamGalleryHeader({ Key? key, required this.message, - this.currentIndex = 0, + required this.attachment, this.showBackButton = true, this.onBackPressed, this.onShowMessage, @@ -60,15 +60,15 @@ class StreamGalleryHeader extends StatelessWidget /// Message which attachments are attached to final Message message; + /// The attachment that's currently in focus + final Attachment attachment; + /// Username of sender final String userName; /// Text which connotes the time the message was sent final String sentAt; - /// Stores the current index of media shown - final int currentIndex; - /// The background color of this [StreamGalleryHeader]. final Color? backgroundColor; @@ -146,14 +146,14 @@ class StreamGalleryHeader extends StatelessWidget StreamChatTheme.of(context).galleryHeaderTheme; final defaultModal = AttachmentActionsModal( + attachment: attachment, message: message, - currentIndex: currentIndex, onShowMessage: onShowMessage, ); final effectiveModal = attachmentActionsModalBuilder?.call( context, - message.attachments[currentIndex], + attachment, defaultModal, ) ?? defaultModal; diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index fa723bef..9a803c88 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -136,10 +136,9 @@ class StreamImageGroup extends StatelessWidget { builder: (context) => StreamChannel( channel: channel, child: StreamFullScreenMedia( - mediaAttachments: images, + mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: index, userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ), diff --git a/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart b/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart new file mode 100644 index 00000000..0ede5142 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart @@ -0,0 +1,18 @@ +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// The [StreamAttachmentPackage] class is basically meant to wrap +/// individual attachments with their corresponding message +class StreamAttachmentPackage { + /// Default constructor to prepare an [StreamAttachmentPackage] object + StreamAttachmentPackage({ + required this.attachment, + required this.message, + }); + + /// This is the individual attachment + final Attachment attachment; + + /// This is the message that the attachment belongs to + /// The message object may have attachemnt(s) other than the one packaged + final Message message; +} diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 2d661fbc..74c721c9 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_attachment_package.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -429,3 +430,19 @@ int levenshtein(String s, String t, {bool caseSensitive = true}) { return v1[t.length]; } + +/// An easy way to handle attachment related operations on a message +extension AttachmentPackagesX on Message { + /// This extension will return a List of type [StreamAttachmentPackage] from the + /// existing attachments of the message + List getAttachmentPackageList() { + final _attachmentPackages = List.generate( + attachments.length, + (index) => StreamAttachmentPackage( + attachment: attachments[index], + message: this, + ), + ); + return _attachmentPackages; + } +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 4b129d1f..50f39721 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -28,10 +28,12 @@ export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; +export 'src/multi_overlay.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; +export 'src/stream_attachment_package.dart'; export 'src/stream_chat.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 87246386..1cb3842e 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: chewie: ^1.3.0 collection: ^1.15.0 diacritic: ^0.1.3 - dio: ^4.0.0 + dio: ^4.0.6 ezanimation: ^0.6.0 file_picker: ^4.1.3 flutter: diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart index 3b711a85..2d42f558 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -9,13 +9,16 @@ import 'mocks.dart'; class MockAttachmentDownloader extends Mock { ProgressCallback? progressCallback; + DownloadedPathCallback? downloadedPathCallback; Completer completer = Completer(); Future call( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }) { this.progressCallback = progressCallback; + this.downloadedPathCallback = downloadedPathCallback; return completer.future; } } @@ -38,6 +41,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'image', + title: 'text.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -45,19 +61,8 @@ void main() { streamChatThemeData: streamTheme, client: client, child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'text.jpg', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -80,6 +85,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'image', + title: 'text.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -87,19 +105,8 @@ void main() { streamChatThemeData: streamTheme, client: client, child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'text.jpg', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -122,6 +129,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -130,19 +150,8 @@ void main() { client: client, child: SizedBox( child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'video', - title: 'video.mp4', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -166,16 +175,17 @@ void main() { final mockObserver = MockNavigatorObserver(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); await tester.pumpWidget( @@ -188,7 +198,7 @@ void main() { child: SizedBox( child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -212,6 +222,20 @@ void main() { final streamTheme = StreamChatThemeData.fromTheme(themeData); final onShowMessage = MockVoidCallback(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); + await tester.pumpWidget( MaterialApp( theme: themeData, @@ -221,18 +245,8 @@ void main() { child: SizedBox( child: AttachmentActionsModal( onShowMessage: onShowMessage, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -255,20 +269,22 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final targetAttachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final remainingAttachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - Attachment( - type: 'image', - title: 'image.jpg', - ), + targetAttachment, + remainingAttachment, ], ); @@ -283,7 +299,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: targetAttachment, ), ), ), @@ -309,16 +325,17 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); @@ -333,7 +350,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -358,15 +375,16 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); @@ -381,7 +399,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -402,6 +420,20 @@ void main() { final imageDownloader = MockAttachmentDownloader(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); + await tester.pumpWidget( MaterialApp( builder: (context, child) => StreamChat( @@ -411,18 +443,8 @@ void main() { home: SizedBox( child: AttachmentActionsModal( imageDownloader: imageDownloader, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -457,6 +479,19 @@ void main() { final fileDownloader = MockAttachmentDownloader(); + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ]); + await tester.pumpWidget( MaterialApp( builder: (context, child) => StreamChat( @@ -466,18 +501,8 @@ void main() { home: SizedBox( child: AttachmentActionsModal( fileDownloader: fileDownloader, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'video', - title: 'video.mp4', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index c17df733..7a815888 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -63,22 +63,29 @@ void main() { Event(type: EventType.typingStart), })); + final attachment = Attachment( + type: 'image', + title: 'demo image', + imageUrl: '', + ); + final message = Message( + createdAt: DateTime.now(), + attachments: [ + attachment, + ], + ); await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, child: StreamFullScreenMedia( - mediaAttachments: [ - Attachment( - type: 'image', - title: 'demo image', - imageUrl: '', + mediaAttachmentPackages: [ + StreamAttachmentPackage( + attachment: attachment, + message: message, ), ], - message: Message( - createdAt: DateTime.now(), - ), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index 7fe611b8..2e2771b4 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -38,7 +38,7 @@ void main() { onWillPop: () async => false, child: Scaffold( body: StreamGalleryFooter( - message: Message(), + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index 0193cb35..f8d1d164 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -75,7 +75,7 @@ void main() { _context = context; return Scaffold( appBar: StreamGalleryFooter( - message: Message(), + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ); }, @@ -118,7 +118,7 @@ void main() { _context = context; return Scaffold( appBar: StreamGalleryFooter( - message: Message(), + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ); }, diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index 0ded6bbf..50be7d57 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -66,9 +66,11 @@ void main() { home: Builder( builder: (context) { _context = context; + final _message = Message(); return Scaffold( appBar: StreamGalleryHeader( - message: Message(), + message: _message, + attachment: _message.attachments[0], ), ); }, @@ -105,9 +107,11 @@ void main() { home: Builder( builder: (context) { _context = context; + final _message = Message(); return Scaffold( appBar: StreamGalleryHeader( - message: Message(), + message: _message, + attachment: _message.attachments[0], ), ); },