diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5d119e8c..76dc371d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -6,6 +6,7 @@ 🐞 Fixed +- [[#766]]`AttachmentActionsModal` now has customisation options for actions. - Fixed `MessageWidget` null errors associated with `channel.memberCount`. - Fixed adding attachments on web. 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 f49561aa..0c10ae46 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget { this.onShowMessage, this.imageDownloader, this.fileDownloader, + this.showReply = true, + this.showShowInChat = true, + this.showSave = true, + this.showDelete = true, + this.customActions = const [], }) : super(key: key); /// The message containing the attachments @@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget { /// Callback to provide download files final AttachmentDownloader? fileDownloader; + /// Show reply option + final bool showReply; + + /// Show show in chat option + final bool showShowInChat; + + /// Show save option + final bool showSave; + + /// Show delete option + final bool showDelete; + + /// List of custom actions + final List customActions; + + /// Creates a copy of [MessageWidget] with specified attributes overridden. + AttachmentActionsModal copyWith({ + Key? key, + int? currentIndex, + Message? message, + VoidCallback? onShowMessage, + AttachmentDownloader? imageDownloader, + AttachmentDownloader? fileDownloader, + bool? showReply, + bool? showShowInChat, + bool? showSave, + bool? showDelete, + List? customActions, + }) => + AttachmentActionsModal( + key: key ?? this.key, + currentIndex: currentIndex ?? this.currentIndex, + message: message ?? this.message, + onShowMessage: onShowMessage ?? this.onShowMessage, + imageDownloader: imageDownloader ?? this.imageDownloader, + fileDownloader: fileDownloader ?? this.fileDownloader, + showReply: showReply ?? this.showReply, + showShowInChat: showShowInChat ?? this.showShowInChat, + showSave: showSave ?? this.showSave, + showDelete: showDelete ?? this.showDelete, + customActions: customActions ?? this.customActions, + ); + @override Widget build(BuildContext context) => GestureDetector( behavior: HitTestBehavior.translucent, @@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ - _buildButton( - context, - context.translations.replyLabel, - StreamSvgIcon.iconCurveLineLeftUp( - size: 24, - color: theme.colorTheme.textLowEmphasis, + if (showReply) + _buildButton( + context, + context.translations.replyLabel, + StreamSvgIcon.iconCurveLineLeftUp( + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + () { + Navigator.pop(context, ReturnActionType.reply); + }, ), - () { - Navigator.pop(context, ReturnActionType.reply); - }, - ), - _buildButton( - context, - context.translations.showInChatLabel, - StreamSvgIcon.eye( - size: 24, - color: theme.colorTheme.textHighEmphasis, + if (showShowInChat) + _buildButton( + context, + context.translations.showInChatLabel, + StreamSvgIcon.eye( + size: 24, + color: theme.colorTheme.textHighEmphasis, + ), + onShowMessage, ), - onShowMessage, - ), - _buildButton( - context, - message.attachments[currentIndex].type == 'video' - ? context.translations.saveVideoLabel - : context.translations.saveImageLabel, - StreamSvgIcon.iconSave( - size: 24, - color: theme.colorTheme.textLowEmphasis, + if (showSave) + _buildButton( + context, + message.attachments[currentIndex].type == 'video' + ? context.translations.saveVideoLabel + : context.translations.saveImageLabel, + StreamSvgIcon.iconSave( + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + () { + final attachment = message.attachments[currentIndex]; + final isImage = attachment.type == 'image'; + final Future Function( + Attachment, { + void Function(int, int) progressCallback, + }) saveFile = fileDownloader ?? _downloadAttachment; + final Future Function( + Attachment, { + void Function(int, int) progressCallback, + }) saveImage = imageDownloader ?? _downloadAttachment; + final downloader = isImage ? saveImage : saveFile; + + final progressNotifier = + ValueNotifier<_DownloadProgress?>( + _DownloadProgress.initial(), + ); + + downloader( + attachment, + progressCallback: (received, total) { + progressNotifier.value = _DownloadProgress( + total, + received, + ); + }, + ).catchError((e, stk) { + progressNotifier.value = null; + }); + + // Closing attachment actions modal before opening + // attachment download dialog + Navigator.pop(context); + + showDialog( + barrierDismissible: false, + context: context, + barrierColor: theme.colorTheme.overlay, + builder: (context) => _buildDownloadProgressDialog( + context, + progressNotifier, + ), + ); + }, ), - () { - final attachment = message.attachments[currentIndex]; - final isImage = attachment.type == 'image'; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - }) saveFile = fileDownloader ?? _downloadAttachment; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - }) saveImage = imageDownloader ?? _downloadAttachment; - final downloader = isImage ? saveImage : saveFile; - - final progressNotifier = - ValueNotifier<_DownloadProgress?>( - _DownloadProgress.initial(), - ); - - downloader( - attachment, - progressCallback: (received, total) { - progressNotifier.value = _DownloadProgress( - total, - received, - ); - }, - ).catchError((e, stk) { - progressNotifier.value = null; - }); - - // Closing attachment actions modal before opening - // attachment download dialog - Navigator.pop(context); - - showDialog( - barrierDismissible: false, - context: context, - barrierColor: theme.colorTheme.overlay, - builder: (context) => _buildDownloadProgressDialog( - context, - progressNotifier, - ), - ); - }, - ), if (StreamChat.of(context).currentUser?.id == - message.user?.id) + message.user?.id && + showDelete) _buildButton( context, context.translations.deleteLabel.capitalize(), @@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget { }, color: theme.colorTheme.accentError, ), + ...customActions + .map( + (e) => _buildButton( + context, + e.actionTitle, + e.icon, + e.onTap, + ), + ) + .toList(), ] .map((e) => Align( alignment: Alignment.centerRight, @@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget { Widget _buildButton( context, String title, - StreamSvgIcon icon, + Widget icon, VoidCallback? onTap, { Color? color, Key? key, @@ -331,3 +393,22 @@ class _DownloadProgress { int get toPercentage => (received * 100) ~/ total; } + +/// Class for custom attachment action +class AttachmentAction { + /// Constructor for custom attachment action + AttachmentAction({ + required this.actionTitle, + required this.icon, + required this.onTap, + }); + + /// Title for the attachment action + String actionTitle; + + /// Icon for the attachment action + Widget icon; + + /// Callback for when the action is tapped + VoidCallback onTap; +} 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 0c47383c..6547a93d 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget { this.startIndex = 0, String? userName, this.onShowMessage, + this.attachmentActionsModalBuilder, }) : userName = userName ?? '', super(key: key); @@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget { /// Callback for when show message is tapped final ShowMessageCallback? onShowMessage; + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + @override _FullScreenMediaState createState() => _FullScreenMediaState(); } @@ -196,6 +202,8 @@ class _FullScreenMediaState extends State StreamChannel.of(context).channel, ); }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, ), if (!widget.message.isEphemeral) GalleryFooter( diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index fc5f67b4..2325af69 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// Widget builder for attachment actions modal +/// [defaultActionsModal] is the default [AttachmentActionsModal] config +/// Use [defaultActionsModal.copyWith] to easily customize it +typedef AttachmentActionsBuilder = Widget Function( + BuildContext context, + Attachment attachment, + AttachmentActionsModal defaultActionsModal, +); + /// Header/AppBar widget for media display screen class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Creates a channel header @@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { this.userName = '', this.sentAt = '', this.backgroundColor, + this.attachmentActionsModalBuilder, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// The background color of this [GalleryHeader]. final Color? backgroundColor; + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); @@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { final galleryHeaderThemeData = StreamChatTheme.of(context).galleryHeaderTheme; + final defaultModal = AttachmentActionsModal( + message: message, + currentIndex: currentIndex, + onShowMessage: onShowMessage, + ); + + final effectiveModal = attachmentActionsModalBuilder?.call( + context, + message.attachments[currentIndex], + defaultModal, + ) ?? + defaultModal; + final result = await showDialog( useRootNavigator: false, context: context, barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor, builder: (context) => StreamChannel( channel: channel, - child: AttachmentActionsModal( - message: message, - currentIndex: currentIndex, - onShowMessage: onShowMessage, - ), + child: effectiveModal, ), ); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 0447fc68..cd0d93a0 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart'; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; +export 'src/attachment_actions_modal.dart'; export 'src/back_button.dart'; export 'src/channel_avatar.dart'; export 'src/channel_header.dart';