Merge pull request #771 from GetStream/feat/attachment-actions
feat(ui): added customisation options for attachment actions modal and related widgets
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#766]]`AttachmentActionsModal` now has customisation options for actions.
|
||||||
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
|
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
|
||||||
- Fixed adding attachments on web.
|
- Fixed adding attachments on web.
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.imageDownloader,
|
this.imageDownloader,
|
||||||
this.fileDownloader,
|
this.fileDownloader,
|
||||||
|
this.showReply = true,
|
||||||
|
this.showShowInChat = true,
|
||||||
|
this.showSave = true,
|
||||||
|
this.showDelete = true,
|
||||||
|
this.customActions = const [],
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The message containing the attachments
|
/// The message containing the attachments
|
||||||
@@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
/// Callback to provide download files
|
/// Callback to provide download files
|
||||||
final AttachmentDownloader? fileDownloader;
|
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<AttachmentAction> 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<AttachmentAction>? 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
|
@override
|
||||||
Widget build(BuildContext context) => GestureDetector(
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
@@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildButton(
|
if (showReply)
|
||||||
context,
|
_buildButton(
|
||||||
context.translations.replyLabel,
|
context,
|
||||||
StreamSvgIcon.iconCurveLineLeftUp(
|
context.translations.replyLabel,
|
||||||
size: 24,
|
StreamSvgIcon.iconCurveLineLeftUp(
|
||||||
color: theme.colorTheme.textLowEmphasis,
|
size: 24,
|
||||||
|
color: theme.colorTheme.textLowEmphasis,
|
||||||
|
),
|
||||||
|
() {
|
||||||
|
Navigator.pop(context, ReturnActionType.reply);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
() {
|
if (showShowInChat)
|
||||||
Navigator.pop(context, ReturnActionType.reply);
|
_buildButton(
|
||||||
},
|
context,
|
||||||
),
|
context.translations.showInChatLabel,
|
||||||
_buildButton(
|
StreamSvgIcon.eye(
|
||||||
context,
|
size: 24,
|
||||||
context.translations.showInChatLabel,
|
color: theme.colorTheme.textHighEmphasis,
|
||||||
StreamSvgIcon.eye(
|
),
|
||||||
size: 24,
|
onShowMessage,
|
||||||
color: theme.colorTheme.textHighEmphasis,
|
|
||||||
),
|
),
|
||||||
onShowMessage,
|
if (showSave)
|
||||||
),
|
_buildButton(
|
||||||
_buildButton(
|
context,
|
||||||
context,
|
message.attachments[currentIndex].type == 'video'
|
||||||
message.attachments[currentIndex].type == 'video'
|
? context.translations.saveVideoLabel
|
||||||
? context.translations.saveVideoLabel
|
: context.translations.saveImageLabel,
|
||||||
: context.translations.saveImageLabel,
|
StreamSvgIcon.iconSave(
|
||||||
StreamSvgIcon.iconSave(
|
size: 24,
|
||||||
size: 24,
|
color: theme.colorTheme.textLowEmphasis,
|
||||||
color: theme.colorTheme.textLowEmphasis,
|
),
|
||||||
|
() {
|
||||||
|
final attachment = message.attachments[currentIndex];
|
||||||
|
final isImage = attachment.type == 'image';
|
||||||
|
final Future<String?> Function(
|
||||||
|
Attachment, {
|
||||||
|
void Function(int, int) progressCallback,
|
||||||
|
}) saveFile = fileDownloader ?? _downloadAttachment;
|
||||||
|
final Future<String?> 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<String?> Function(
|
|
||||||
Attachment, {
|
|
||||||
void Function(int, int) progressCallback,
|
|
||||||
}) saveFile = fileDownloader ?? _downloadAttachment;
|
|
||||||
final Future<String?> 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 ==
|
if (StreamChat.of(context).currentUser?.id ==
|
||||||
message.user?.id)
|
message.user?.id &&
|
||||||
|
showDelete)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
context.translations.deleteLabel.capitalize(),
|
context.translations.deleteLabel.capitalize(),
|
||||||
@@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
color: theme.colorTheme.accentError,
|
color: theme.colorTheme.accentError,
|
||||||
),
|
),
|
||||||
|
...customActions
|
||||||
|
.map(
|
||||||
|
(e) => _buildButton(
|
||||||
|
context,
|
||||||
|
e.actionTitle,
|
||||||
|
e.icon,
|
||||||
|
e.onTap,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
]
|
]
|
||||||
.map<Widget>((e) => Align(
|
.map<Widget>((e) => Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
Widget _buildButton(
|
Widget _buildButton(
|
||||||
context,
|
context,
|
||||||
String title,
|
String title,
|
||||||
StreamSvgIcon icon,
|
Widget icon,
|
||||||
VoidCallback? onTap, {
|
VoidCallback? onTap, {
|
||||||
Color? color,
|
Color? color,
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -331,3 +393,22 @@ class _DownloadProgress {
|
|||||||
|
|
||||||
int get toPercentage => (received * 100) ~/ total;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
this.startIndex = 0,
|
this.startIndex = 0,
|
||||||
String? userName,
|
String? userName,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : userName = userName ?? '',
|
}) : userName = userName ?? '',
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
/// Callback for when show message is tapped
|
/// Callback for when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
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
|
@override
|
||||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||||
}
|
}
|
||||||
@@ -196,6 +202,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
StreamChannel.of(context).channel,
|
StreamChannel.of(context).channel,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
attachmentActionsModalBuilder:
|
||||||
|
widget.attachmentActionsModalBuilder,
|
||||||
),
|
),
|
||||||
if (!widget.message.isEphemeral)
|
if (!widget.message.isEphemeral)
|
||||||
GalleryFooter(
|
GalleryFooter(
|
||||||
|
|||||||
@@ -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/src/theme/themes.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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
|
/// Header/AppBar widget for media display screen
|
||||||
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
@@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.userName = '',
|
this.userName = '',
|
||||||
this.sentAt = '',
|
this.sentAt = '',
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// The background color of this [GalleryHeader].
|
/// The background color of this [GalleryHeader].
|
||||||
final Color? backgroundColor;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||||
@@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final galleryHeaderThemeData =
|
final galleryHeaderThemeData =
|
||||||
StreamChatTheme.of(context).galleryHeaderTheme;
|
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(
|
final result = await showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: AttachmentActionsModal(
|
child: effectiveModal,
|
||||||
message: message,
|
|
||||||
currentIndex: currentIndex,
|
|
||||||
onShowMessage: onShowMessage,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
|
|||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
|
export 'src/attachment_actions_modal.dart';
|
||||||
export 'src/back_button.dart';
|
export 'src/back_button.dart';
|
||||||
export 'src/channel_avatar.dart';
|
export 'src/channel_avatar.dart';
|
||||||
export 'src/channel_header.dart';
|
export 'src/channel_header.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user