Implemented StreamAttachmentPackage and fixed operations in widgets linked to full screen implementation, also fixed tests

This commit is contained in:
Ayush Shekhar
2022-04-13 18:02:49 +05:30
parent 65a5af7b6c
commit 180c24ae72
18 changed files with 373 additions and 253 deletions
-1
View File
@@ -91,7 +91,6 @@ linter:
- prefer_constructors_over_static_methods - prefer_constructors_over_static_methods
- prefer_contains - prefer_contains
- prefer_equal_for_default_values - prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields - prefer_final_fields
- prefer_final_in_for_each - prefer_final_in_for_each
- prefer_final_locals - prefer_final_locals
@@ -250,10 +250,9 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: StreamFullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachmentPackages: message.getAttachmentPackageList(),
startIndex: message.attachments.indexOf(attachment), startIndex: message.attachments.indexOf(attachment),
userName: message.user?.name, userName: message.user?.name,
message: message,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
), ),
); );
@@ -144,11 +144,11 @@ class StreamImageAttachment extends StreamAttachmentWidget {
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: StreamFullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachmentPackages:
message.getAttachmentPackageList(),
startIndex: startIndex:
message.attachments.indexOf(attachment), message.attachments.indexOf(attachment),
userName: message.user?.name, userName: message.user?.name,
message: message,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
), ),
); );
@@ -91,11 +91,11 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
builder: (_) => StreamChannel( builder: (_) => StreamChannel(
channel: channel, channel: channel,
child: StreamFullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachmentPackages:
message.getAttachmentPackageList(),
startIndex: startIndex:
message.attachments.indexOf(attachment), message.attachments.indexOf(attachment),
userName: message.user?.name, userName: message.user?.name,
message: message,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
), ),
), ),
@@ -9,14 +9,18 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
typedef AttachmentDownloader = Future<String> Function( typedef AttachmentDownloader = Future<String> Function(
Attachment attachment, { Attachment attachment, {
ProgressCallback? progressCallback, 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 /// Widget that shows the options in the gallery view
class AttachmentActionsModal extends StatelessWidget { class AttachmentActionsModal extends StatelessWidget {
/// Returns a new [AttachmentActionsModal] /// Returns a new [AttachmentActionsModal]
const AttachmentActionsModal({ const AttachmentActionsModal({
Key? key, Key? key,
required this.currentIndex, required this.attachment,
required this.message, required this.message,
this.onShowMessage, this.onShowMessage,
this.imageDownloader, this.imageDownloader,
@@ -28,12 +32,12 @@ class AttachmentActionsModal extends StatelessWidget {
this.customActions = const [], this.customActions = const [],
}) : super(key: key); }) : super(key: key);
/// The attachment object for which the actions are to be performed
final Attachment attachment;
/// The message containing the attachments /// The message containing the attachments
final Message message; final Message message;
/// Current page index
final int currentIndex;
/// Callback to show the message /// Callback to show the message
final VoidCallback? onShowMessage; final VoidCallback? onShowMessage;
@@ -62,7 +66,7 @@ class AttachmentActionsModal extends StatelessWidget {
/// specified attributes overridden. /// specified attributes overridden.
AttachmentActionsModal copyWith({ AttachmentActionsModal copyWith({
Key? key, Key? key,
int? currentIndex, Attachment? attachment,
Message? message, Message? message,
VoidCallback? onShowMessage, VoidCallback? onShowMessage,
AttachmentDownloader? imageDownloader, AttachmentDownloader? imageDownloader,
@@ -75,7 +79,7 @@ class AttachmentActionsModal extends StatelessWidget {
}) => }) =>
AttachmentActionsModal( AttachmentActionsModal(
key: key ?? this.key, key: key ?? this.key,
currentIndex: currentIndex ?? this.currentIndex, attachment: attachment ?? this.attachment,
message: message ?? this.message, message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage, onShowMessage: onShowMessage ?? this.onShowMessage,
imageDownloader: imageDownloader ?? this.imageDownloader, imageDownloader: imageDownloader ?? this.imageDownloader,
@@ -138,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget {
if (showSave) if (showSave)
_buildButton( _buildButton(
context, context,
message.attachments[currentIndex].type == 'video' attachment.type == 'video'
? context.translations.saveVideoLabel ? context.translations.saveVideoLabel
: context.translations.saveImageLabel, : context.translations.saveImageLabel,
StreamSvgIcon.iconSave( StreamSvgIcon.iconSave(
@@ -146,15 +150,16 @@ class AttachmentActionsModal extends StatelessWidget {
color: theme.colorTheme.textLowEmphasis, color: theme.colorTheme.textLowEmphasis,
), ),
() { () {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image'; final isImage = attachment.type == 'image';
final Future<String?> Function( final Future<String?> Function(
Attachment, { Attachment, {
void Function(int, int) progressCallback, void Function(int, int) progressCallback,
DownloadedPathCallback downloadedPathCallback,
}) saveFile = fileDownloader ?? _downloadAttachment; }) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function( final Future<String?> Function(
Attachment, { Attachment, {
void Function(int, int) progressCallback, void Function(int, int) progressCallback,
DownloadedPathCallback downloadedPathCallback,
}) saveImage = imageDownloader ?? _downloadAttachment; }) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile; final downloader = isImage ? saveImage : saveFile;
@@ -162,15 +167,25 @@ class AttachmentActionsModal extends StatelessWidget {
ValueNotifier<_DownloadProgress?>( ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(), _DownloadProgress.initial(),
); );
final downloadedPathNotifier = ValueNotifier<String?>(
null,
);
downloader( downloader(
attachment, attachment,
progressCallback: (received, total) { progressCallback: (received, total) {
print('>>>>>>>>>>>>>>>');
print('>>r : $received');
print('>>t : $total');
print('>>>>>>>>>>>>>>>');
progressNotifier.value = _DownloadProgress( progressNotifier.value = _DownloadProgress(
total, total,
received, received,
); );
}, },
downloadedPathCallback: (String? path) {
downloadedPathNotifier.value = path;
},
).catchError((e, stk) { ).catchError((e, stk) {
progressNotifier.value = null; progressNotifier.value = null;
}); });
@@ -186,6 +201,7 @@ class AttachmentActionsModal extends StatelessWidget {
builder: (context) => _buildDownloadProgressDialog( builder: (context) => _buildDownloadProgressDialog(
context, context,
progressNotifier, progressNotifier,
downloadedPathNotifier,
), ),
); );
}, },
@@ -204,8 +220,12 @@ class AttachmentActionsModal extends StatelessWidget {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
if (message.attachments.length > 1 || if (message.attachments.length > 1 ||
message.text?.isNotEmpty == true) { message.text?.isNotEmpty == true) {
final currentAttachmentIndex =
message.attachments.indexWhere(
(element) => element.id == attachment.id,
);
final remainingAttachments = [...message.attachments] final remainingAttachments = [...message.attachments]
..removeAt(currentIndex); ..removeAt(currentAttachmentIndex);
channel.updateMessage(message.copyWith( channel.updateMessage(message.copyWith(
attachments: remainingAttachments, attachments: remainingAttachments,
)); ));
@@ -285,73 +305,86 @@ class AttachmentActionsModal extends StatelessWidget {
Widget _buildDownloadProgressDialog( Widget _buildDownloadProgressDialog(
BuildContext context, BuildContext context,
ValueNotifier<_DownloadProgress?> progressNotifier, ValueNotifier<_DownloadProgress?> progressNotifier,
ValueNotifier<String?> downloadedFilePathNotifier,
) { ) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: progressNotifier, valueListenable: downloadedFilePathNotifier,
builder: (_, _DownloadProgress? progress, __) { builder: (_, String? path, __) {
// Pop the dialog in case the progress is null or it's completed. final _downloadComplete = path != null && path.isNotEmpty;
if (progress == null || progress.toProgressIndicatorValue == 1.0) { // Pop the dialog in case the download has completed
if (_downloadComplete) {
Future.delayed( Future.delayed(
const Duration(milliseconds: 500), const Duration(milliseconds: 500),
() => Navigator.of(context).maybePop(), () => Navigator.of(context).maybePop(),
); );
} }
return Material( return ValueListenableBuilder(
type: MaterialType.transparency, valueListenable: progressNotifier,
child: Center( builder: (_, _DownloadProgress? progress, __) {
child: Container( // Pop the dialog in case the progress is null.
height: 182, if (progress == null) {
width: 182, Future.delayed(
decoration: BoxDecoration( const Duration(milliseconds: 500),
borderRadius: BorderRadius.circular(16), () => Navigator.of(context).maybePop(),
color: theme.colorTheme.barsBg, );
), }
return Material(
type: MaterialType.transparency,
child: Center( child: Center(
child: progress == null child: Container(
? SizedBox( height: 182,
height: 100, width: 182,
width: 100, decoration: BoxDecoration(
child: StreamSvgIcon.error( borderRadius: BorderRadius.circular(16),
color: theme.colorTheme.disabled, color: theme.colorTheme.barsBg,
), ),
) child: Center(
: progress.toProgressIndicatorValue == 1.0 child: progress == null
? SizedBox( ? SizedBox(
key: const Key('completedIcon'), height: 100,
height: 160, width: 100,
width: 160, child: StreamSvgIcon.error(
child: StreamSvgIcon.check(
color: theme.colorTheme.disabled, color: theme.colorTheme.disabled,
), ),
) )
: SizedBox( : _downloadComplete
height: 100, ? SizedBox(
width: 100, key: const Key('completedIcon'),
child: Stack( height: 160,
fit: StackFit.expand, width: 160,
children: [ child: StreamSvgIcon.check(
CircularProgressIndicator( color: theme.colorTheme.disabled,
value: progress.toProgressIndicatorValue,
strokeWidth: 8,
valueColor: AlwaysStoppedAnimation(
theme.colorTheme.accentPrimary,
),
), ),
Center( )
child: Text( : SizedBox(
'${progress.toPercentage}%', height: 100,
style: theme.textTheme.headline.copyWith( width: 100,
color: theme.colorTheme.textLowEmphasis, 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<String?> _downloadAttachment( Future<String?> _downloadAttachment(
Attachment attachment, { Attachment attachment, {
ProgressCallback? progressCallback, ProgressCallback? progressCallback,
DownloadedPathCallback? downloadedPathCallback,
}) async { }) async {
String? filePath; String? filePath;
final appDocDir = await getTemporaryDirectory(); final appDocDir = await getTemporaryDirectory();
@@ -375,6 +409,7 @@ class AttachmentActionsModal extends StatelessWidget {
onReceiveProgress: progressCallback, onReceiveProgress: progressCallback,
); );
final result = await ImageGallerySaver.saveFile(filePath!); final result = await ImageGallerySaver.saveFile(filePath!);
downloadedPathCallback?.call((result as Map)['filePath']);
return (result as Map)['filePath']; return (result as Map)['filePath'];
} }
} }
@@ -388,6 +423,8 @@ class _DownloadProgress {
final int total; final int total;
final int received; final int received;
String get receivedValueInMB => ((received / 1024) / 1024).toStringAsFixed(2);
double get toProgressIndicatorValue => received / total; double get toProgressIndicatorValue => received / total;
int get toPercentage => (received * 100) ~/ total; int get toPercentage => (received * 100) ~/ total;
@@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@@ -32,8 +33,7 @@ class StreamFullScreenMedia extends StatefulWidget {
/// Instantiate a new FullScreenImage /// Instantiate a new FullScreenImage
const StreamFullScreenMedia({ const StreamFullScreenMedia({
Key? key, Key? key,
required this.mediaAttachments, required this.mediaAttachmentPackages,
required this.message,
this.startIndex = 0, this.startIndex = 0,
String? userName, String? userName,
this.onShowMessage, this.onShowMessage,
@@ -43,10 +43,7 @@ class StreamFullScreenMedia extends StatefulWidget {
super(key: key); super(key: key);
/// The url of the image /// The url of the image
final List<Attachment> mediaAttachments; final List<StreamAttachmentPackage> mediaAttachmentPackages;
/// Message where attachments are attached
final Message message;
/// First index of media shown /// First index of media shown
final int startIndex; final int startIndex;
@@ -100,8 +97,8 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
); );
_pageController = PageController(initialPage: widget.startIndex); _pageController = PageController(initialPage: widget.startIndex);
for (var i = 0; i < widget.mediaAttachments.length; i++) { for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
final attachment = widget.mediaAttachments[i]; final attachment = widget.mediaAttachmentPackages[i].attachment;
if (attachment.type != 'video') continue; if (attachment.type != 'video') continue;
final package = VideoPackage(attachment, showControls: true); final package = VideoPackage(attachment, showControls: true);
videoPackages[attachment.id] = package; videoPackages[attachment.id] = package;
@@ -114,7 +111,8 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
return; return;
} }
final currentAttachment = widget.mediaAttachments[widget.startIndex]; final currentAttachment =
widget.mediaAttachmentPackages[widget.startIndex].attachment;
await Future.wait(videoPackages.values.map( await Future.wait(videoPackages.values.map(
(it) => it.initialize(), (it) => it.initialize(),
@@ -142,7 +140,8 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
return; return;
} }
final currentAttachment = widget.mediaAttachments[val]; final currentAttachment =
widget.mediaAttachmentPackages[val].attachment;
for (final e in videoPackages.values) { for (final e in videoPackages.values) {
if (e._attachment != currentAttachment) { if (e._attachment != currentAttachment) {
@@ -157,7 +156,9 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
} }
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
final attachment = widget.mediaAttachments[index]; final currentAttachmentPackage =
widget.mediaAttachmentPackages[index];
final attachment = currentAttachmentPackage.attachment;
if (attachment.type == 'image' || attachment.type == 'giphy') { if (attachment.type == 'image' || attachment.type == 'giphy') {
final imageUrl = attachment.imageUrl ?? final imageUrl = attachment.imageUrl ??
attachment.assetUrl ?? attachment.assetUrl ??
@@ -174,7 +175,7 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
maxScale: PhotoViewComputedScale.covered, maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments, tag: widget.mediaAttachmentPackages,
), ),
backgroundDecoration: BoxDecoration( backgroundDecoration: BoxDecoration(
color: ColorTween( color: ColorTween(
@@ -218,53 +219,66 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
} }
return const SizedBox(); return const SizedBox();
}, },
itemCount: widget.mediaAttachments.length, itemCount: widget.mediaAttachmentPackages.length,
), ),
FadeTransition( FadeTransition(
opacity: _opacityAnimation, opacity: _opacityAnimation,
child: ValueListenableBuilder<int>( child: ValueListenableBuilder<int>(
valueListenable: _currentPage, valueListenable: _currentPage,
builder: (context, value, child) => Column( builder: (context, value, child) {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final _currentAttachmentPackage =
children: [ widget.mediaAttachmentPackages[value];
StreamGalleryHeader( final _currentMessage = _currentAttachmentPackage.message;
userName: widget.userName, final _currentAttachment =
sentAt: context.translations.sentAtText( _currentAttachmentPackage.attachment;
date: widget.message.createdAt, return Column(
time: widget.message.createdAt, mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
onBackPressed: () { StreamGalleryHeader(
Navigator.of(context).pop(); userName: widget.userName,
}, sentAt: context.translations.sentAtText(
message: widget.message, date: widget
currentIndex: value, .mediaAttachmentPackages[_currentPage.value]
onShowMessage: () { .message
widget.onShowMessage?.call( .createdAt,
widget.message, time: widget
StreamChannel.of(context).channel, .mediaAttachmentPackages[_currentPage.value]
); .message
}, .createdAt,
attachmentActionsModalBuilder: ),
widget.attachmentActionsModalBuilder, onBackPressed: () {
), Navigator.of(context).pop();
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);
}, },
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);
},
),
],
);
},
), ),
), ),
], ],
@@ -18,16 +18,15 @@ typedef GalleryFooter = StreamGalleryFooter;
/// {@endtemplate} /// {@endtemplate}
class StreamGalleryFooter extends StatefulWidget class StreamGalleryFooter extends StatefulWidget
implements PreferredSizeWidget { implements PreferredSizeWidget {
/// Creates a channel header /// Creates a StreamGalleryFooter
const StreamGalleryFooter({ const StreamGalleryFooter({
Key? key, Key? key,
required this.message,
this.onBackPressed, this.onBackPressed,
this.onTitleTap, this.onTitleTap,
this.onImageTap, this.onImageTap,
this.currentPage = 0, this.currentPage = 0,
this.totalPages = 0, this.totalPages = 0,
this.mediaAttachments = const [], required this.mediaAttachmentPackages,
this.mediaSelectedCallBack, this.mediaSelectedCallBack,
this.backgroundColor, this.backgroundColor,
}) : preferredSize = const Size.fromHeight(kToolbarHeight), }) : preferredSize = const Size.fromHeight(kToolbarHeight),
@@ -50,10 +49,7 @@ class StreamGalleryFooter extends StatefulWidget
final int totalPages; final int totalPages;
/// All attachments to show /// All attachments to show
final List<Attachment> mediaAttachments; final List<StreamAttachmentPackage> mediaAttachmentPackages;
/// Message which attachments are attached to
final Message message;
/// Callback when media is selected /// Callback when media is selected
final ValueChanged<int>? mediaSelectedCallBack; final ValueChanged<int>? mediaSelectedCallBack;
@@ -99,8 +95,8 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
color: galleryFooterThemeData.shareIconColor, color: galleryFooterThemeData.shareIconColor,
), ),
onPressed: () async { onPressed: () async {
final attachment = final attachment = widget
widget.mediaAttachments[widget.currentPage]; .mediaAttachmentPackages[widget.currentPage].attachment;
final url = attachment.imageUrl ?? final url = attachment.imageUrl ??
attachment.assetUrl ?? attachment.assetUrl ??
attachment.thumbUrl!; attachment.thumbUrl!;
@@ -171,7 +167,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
builder: (context) { builder: (context) {
const crossAxisCount = 3; const crossAxisCount = 3;
final noOfRowToShowInitially = final noOfRowToShowInitially =
widget.mediaAttachments.length > crossAxisCount ? 2 : 1; widget.mediaAttachmentPackages.length > crossAxisCount ? 2 : 1;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final initialChildSize = final initialChildSize =
48 + (size.width * noOfRowToShowInitially) / crossAxisCount; 48 + (size.width * noOfRowToShowInitially) / crossAxisCount;
@@ -212,7 +208,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
child: GridView.builder( child: GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: widget.mediaAttachments.length, itemCount: widget.mediaAttachmentPackages.length,
padding: const EdgeInsets.all(1), padding: const EdgeInsets.all(1),
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
gridDelegate: gridDelegate:
@@ -223,7 +219,10 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
Widget media; Widget media;
final attachment = widget.mediaAttachments[index]; final attachmentPackage =
widget.mediaAttachmentPackages[index];
final attachment = attachmentPackage.attachment;
final message = attachmentPackage.message;
if (attachment.type == 'video') { if (attachment.type == 'video') {
media = InkWell( media = InkWell(
onTap: () => widget.mediaSelectedCallBack!(index), onTap: () => widget.mediaSelectedCallBack!(index),
@@ -253,7 +252,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
return Stack( return Stack(
children: [ children: [
media, media,
if (widget.message.user != null) if (message.user != null)
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Container( child: Container(
@@ -272,7 +271,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
], ],
), ),
child: StreamUserAvatar( child: StreamUserAvatar(
user: widget.message.user!, user: message.user!,
constraints: constraints:
BoxConstraints.tight(const Size(24, 24)), BoxConstraints.tight(const Size(24, 24)),
showOnlineStatus: false, showOnlineStatus: false,
@@ -28,7 +28,7 @@ class StreamGalleryHeader extends StatelessWidget
const StreamGalleryHeader({ const StreamGalleryHeader({
Key? key, Key? key,
required this.message, required this.message,
this.currentIndex = 0, required this.attachment,
this.showBackButton = true, this.showBackButton = true,
this.onBackPressed, this.onBackPressed,
this.onShowMessage, this.onShowMessage,
@@ -60,15 +60,15 @@ class StreamGalleryHeader extends StatelessWidget
/// Message which attachments are attached to /// Message which attachments are attached to
final Message message; final Message message;
/// The attachment that's currently in focus
final Attachment attachment;
/// Username of sender /// Username of sender
final String userName; final String userName;
/// Text which connotes the time the message was sent /// Text which connotes the time the message was sent
final String sentAt; final String sentAt;
/// Stores the current index of media shown
final int currentIndex;
/// The background color of this [StreamGalleryHeader]. /// The background color of this [StreamGalleryHeader].
final Color? backgroundColor; final Color? backgroundColor;
@@ -146,14 +146,14 @@ class StreamGalleryHeader extends StatelessWidget
StreamChatTheme.of(context).galleryHeaderTheme; StreamChatTheme.of(context).galleryHeaderTheme;
final defaultModal = AttachmentActionsModal( final defaultModal = AttachmentActionsModal(
attachment: attachment,
message: message, message: message,
currentIndex: currentIndex,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
); );
final effectiveModal = attachmentActionsModalBuilder?.call( final effectiveModal = attachmentActionsModalBuilder?.call(
context, context,
message.attachments[currentIndex], attachment,
defaultModal, defaultModal,
) ?? ) ??
defaultModal; defaultModal;
@@ -136,10 +136,9 @@ class StreamImageGroup extends StatelessWidget {
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: StreamFullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: images, mediaAttachmentPackages: message.getAttachmentPackageList(),
startIndex: index, startIndex: index,
userName: message.user?.name, userName: message.user?.name,
message: message,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
), ),
), ),
@@ -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;
}
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:url_launcher/url_launcher.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]; 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<StreamAttachmentPackage> getAttachmentPackageList() {
final _attachmentPackages = List<StreamAttachmentPackage>.generate(
attachments.length,
(index) => StreamAttachmentPackage(
attachment: attachments[index],
message: this,
),
);
return _attachmentPackages;
}
}
@@ -28,10 +28,12 @@ export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart'; export 'src/message_search_list_view.dart';
export 'src/message_text.dart'; export 'src/message_text.dart';
export 'src/message_widget.dart'; export 'src/message_widget.dart';
export 'src/multi_overlay.dart';
export 'src/option_list_tile.dart'; export 'src/option_list_tile.dart';
export 'src/reaction_icon.dart'; export 'src/reaction_icon.dart';
export 'src/reaction_picker.dart'; export 'src/reaction_picker.dart';
export 'src/sending_indicator.dart'; export 'src/sending_indicator.dart';
export 'src/stream_attachment_package.dart';
export 'src/stream_chat.dart'; export 'src/stream_chat.dart';
export 'src/stream_chat_theme.dart'; export 'src/stream_chat_theme.dart';
export 'src/stream_neumorphic_button.dart'; export 'src/stream_neumorphic_button.dart';
+1 -1
View File
@@ -15,7 +15,7 @@ dependencies:
chewie: ^1.3.0 chewie: ^1.3.0
collection: ^1.15.0 collection: ^1.15.0
diacritic: ^0.1.3 diacritic: ^0.1.3
dio: ^4.0.0 dio: ^4.0.6
ezanimation: ^0.6.0 ezanimation: ^0.6.0
file_picker: ^4.1.3 file_picker: ^4.1.3
flutter: flutter:
@@ -9,13 +9,16 @@ import 'mocks.dart';
class MockAttachmentDownloader extends Mock { class MockAttachmentDownloader extends Mock {
ProgressCallback? progressCallback; ProgressCallback? progressCallback;
DownloadedPathCallback? downloadedPathCallback;
Completer<String> completer = Completer(); Completer<String> completer = Completer();
Future<String> call( Future<String> call(
Attachment attachment, { Attachment attachment, {
ProgressCallback? progressCallback, ProgressCallback? progressCallback,
DownloadedPathCallback? downloadedPathCallback,
}) { }) {
this.progressCallback = progressCallback; this.progressCallback = progressCallback;
this.downloadedPathCallback = downloadedPathCallback;
return completer.future; return completer.future;
} }
} }
@@ -38,6 +41,19 @@ void main() {
final themeData = ThemeData(); final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(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( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: themeData, theme: themeData,
@@ -45,19 +61,8 @@ void main() {
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'image',
title: 'text.jpg',
),
],
),
currentIndex: 0,
), ),
), ),
), ),
@@ -80,6 +85,19 @@ void main() {
final themeData = ThemeData(); final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(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( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: themeData, theme: themeData,
@@ -87,19 +105,8 @@ void main() {
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
client: client, client: client,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'image',
title: 'text.jpg',
),
],
),
currentIndex: 0,
), ),
), ),
), ),
@@ -122,6 +129,19 @@ void main() {
final themeData = ThemeData(); final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(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( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: themeData, theme: themeData,
@@ -130,19 +150,8 @@ void main() {
client: client, client: client,
child: SizedBox( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'video',
title: 'video.mp4',
),
],
),
currentIndex: 0,
), ),
), ),
), ),
@@ -166,16 +175,17 @@ void main() {
final mockObserver = MockNavigatorObserver(); final mockObserver = MockNavigatorObserver();
final attachment = Attachment(
type: 'image',
title: 'image.jpg',
);
final message = Message( final message = Message(
text: 'test', text: 'test',
user: User( user: User(
id: 'user-id', id: 'user-id',
), ),
attachments: [ attachments: [
Attachment( attachment,
type: 'image',
title: 'image.jpg',
),
], ],
); );
await tester.pumpWidget( await tester.pumpWidget(
@@ -188,7 +198,7 @@ void main() {
child: SizedBox( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: 0, attachment: attachment,
), ),
), ),
), ),
@@ -212,6 +222,20 @@ void main() {
final streamTheme = StreamChatThemeData.fromTheme(themeData); final streamTheme = StreamChatThemeData.fromTheme(themeData);
final onShowMessage = MockVoidCallback(); 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: themeData, theme: themeData,
@@ -221,18 +245,8 @@ void main() {
child: SizedBox( child: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'image',
title: 'image.jpg',
),
]),
currentIndex: 0,
), ),
), ),
), ),
@@ -255,20 +269,22 @@ void main() {
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); 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( final message = Message(
text: 'test', text: 'test',
user: User( user: User(
id: 'user-id', id: 'user-id',
), ),
attachments: [ attachments: [
Attachment( targetAttachment,
type: 'image', remainingAttachment,
title: 'image.jpg',
),
Attachment(
type: 'image',
title: 'image.jpg',
),
], ],
); );
@@ -283,7 +299,7 @@ void main() {
channel: mockChannel, channel: mockChannel,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: 0, attachment: targetAttachment,
), ),
), ),
), ),
@@ -309,16 +325,17 @@ void main() {
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
final attachment = Attachment(
type: 'image',
title: 'image.jpg',
);
final message = Message( final message = Message(
text: 'test', text: 'test',
user: User( user: User(
id: 'user-id', id: 'user-id',
), ),
attachments: [ attachments: [
Attachment( attachment,
type: 'image',
title: 'image.jpg',
),
], ],
); );
@@ -333,7 +350,7 @@ void main() {
channel: mockChannel, channel: mockChannel,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: 0, attachment: attachment,
), ),
), ),
), ),
@@ -358,15 +375,16 @@ void main() {
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
final attachment = Attachment(
type: 'image',
title: 'image.jpg',
);
final message = Message( final message = Message(
user: User( user: User(
id: 'user-id', id: 'user-id',
), ),
attachments: [ attachments: [
Attachment( attachment,
type: 'image',
title: 'image.jpg',
),
], ],
); );
@@ -381,7 +399,7 @@ void main() {
channel: mockChannel, channel: mockChannel,
child: AttachmentActionsModal( child: AttachmentActionsModal(
message: message, message: message,
currentIndex: 0, attachment: attachment,
), ),
), ),
), ),
@@ -402,6 +420,20 @@ void main() {
final imageDownloader = MockAttachmentDownloader(); 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) => StreamChat( builder: (context, child) => StreamChat(
@@ -411,18 +443,8 @@ void main() {
home: SizedBox( home: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
imageDownloader: imageDownloader, imageDownloader: imageDownloader,
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'image',
title: 'image.jpg',
),
]),
currentIndex: 0,
), ),
), ),
), ),
@@ -457,6 +479,19 @@ void main() {
final fileDownloader = MockAttachmentDownloader(); 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
builder: (context, child) => StreamChat( builder: (context, child) => StreamChat(
@@ -466,18 +501,8 @@ void main() {
home: SizedBox( home: SizedBox(
child: AttachmentActionsModal( child: AttachmentActionsModal(
fileDownloader: fileDownloader, fileDownloader: fileDownloader,
message: Message( message: message,
text: 'test', attachment: attachment,
user: User(
id: 'user-id',
),
attachments: [
Attachment(
type: 'video',
title: 'video.mp4',
),
]),
currentIndex: 0,
), ),
), ),
), ),
@@ -63,22 +63,29 @@ void main() {
Event(type: EventType.typingStart), 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( await tester.pumpWidget(MaterialApp(
home: StreamChat( home: StreamChat(
client: client, client: client,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: StreamFullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: [ mediaAttachmentPackages: [
Attachment( StreamAttachmentPackage(
type: 'image', attachment: attachment,
title: 'demo image', message: message,
imageUrl: '',
), ),
], ],
message: Message(
createdAt: DateTime.now(),
),
), ),
), ),
), ),
@@ -38,7 +38,7 @@ void main() {
onWillPop: () async => false, onWillPop: () async => false,
child: Scaffold( child: Scaffold(
body: StreamGalleryFooter( body: StreamGalleryFooter(
message: Message(), mediaAttachmentPackages: Message().getAttachmentPackageList(),
), ),
), ),
), ),
@@ -75,7 +75,7 @@ void main() {
_context = context; _context = context;
return Scaffold( return Scaffold(
appBar: StreamGalleryFooter( appBar: StreamGalleryFooter(
message: Message(), mediaAttachmentPackages: Message().getAttachmentPackageList(),
), ),
); );
}, },
@@ -118,7 +118,7 @@ void main() {
_context = context; _context = context;
return Scaffold( return Scaffold(
appBar: StreamGalleryFooter( appBar: StreamGalleryFooter(
message: Message(), mediaAttachmentPackages: Message().getAttachmentPackageList(),
), ),
); );
}, },
@@ -66,9 +66,11 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
final _message = Message();
return Scaffold( return Scaffold(
appBar: StreamGalleryHeader( appBar: StreamGalleryHeader(
message: Message(), message: _message,
attachment: _message.attachments[0],
), ),
); );
}, },
@@ -105,9 +107,11 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
final _message = Message();
return Scaffold( return Scaffold(
appBar: StreamGalleryHeader( appBar: StreamGalleryHeader(
message: Message(), message: _message,
attachment: _message.attachments[0],
), ),
); );
}, },