diff --git a/assets/icon_edit.svg b/assets/icon_edit.svg new file mode 100644 index 00000000..6979c575 --- /dev/null +++ b/assets/icon_edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart new file mode 100644 index 00000000..5a089786 --- /dev/null +++ b/lib/src/message_actions_modal.dart @@ -0,0 +1,278 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/reaction_picker.dart'; +import 'package:stream_chat_flutter/src/stream_channel.dart'; + +import '../stream_chat_flutter.dart'; +import 'message_input.dart'; +import 'stream_chat.dart'; + +class MessageActionsModal extends StatelessWidget { + final Widget Function(BuildContext, Message) editMessageInputBuilder; + final void Function(Message) onThreadTap; + final Message message; + final MessageTheme messageTheme; + final bool showReactions; + final bool showDeleteMessage; + final bool showEditMessage; + final bool showReply; + final bool reverse; + final ShapeBorder messageShape; + final Map reactionToEmoji = const { + 'love': '❤️️', + 'haha': '😂', + 'like': '👍', + 'sad': '😕', + 'angry': '😡', + 'wow': '😲', + }; + + const MessageActionsModal({ + Key key, + @required this.message, + @required this.messageTheme, + this.showReactions, + this.showDeleteMessage, + this.showEditMessage, + this.onThreadTap, + this.showReply, + this.editMessageInputBuilder, + this.messageShape, + this.reverse, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; + return Stack( + children: [ + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.pop(context); + }, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + ReactionPicker( + channel: channel, + reactionToEmoji: reactionToEmoji, + message: message, + ), + AbsorbPointer( + child: MessageWidget( + reverse: reverse, + message: message, + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showReplyIndicator: false, + showTimestamp: false, + showSendingIndicator: DisplayWidget.gone, + shape: messageShape, + ), + ), + SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 48.0, + ), + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: ListTile.divideTiles( + context: context, + tiles: [ + if (showEditMessage) _buildEditMessage(context), + if (showReply && + (message.status == MessageSendingStatus.SENT || + message.status == null) && + message.parentId == null) + _buildReplyButton(context), + if (showDeleteMessage) _buildDeleteButton(context), + ], + ).toList(), + ), + ), + ) + ], + ), + ], + ); + } + + Widget _buildDeleteButton(BuildContext context) { + return ListTile( + title: Text( + 'Delete message', + style: + Theme.of(context).textTheme.headline6.copyWith(color: Colors.red), + ), + leading: Icon( + Icons.delete_outline, + color: Colors.red, + ), + onTap: () { + Navigator.pop(context); + StreamChat.of(context).client.deleteMessage( + message, + StreamChannel.of(context).channel.cid, + ); + }, + ); + } + + Widget _buildEditMessage(BuildContext context) { + return ListTile( + title: Text( + 'Edit message', + style: Theme.of(context).textTheme.headline6, + ), + leading: SvgPicture.asset( + 'assets/icon_edit.svg', + alignment: Alignment.center, + package: 'stream_chat_flutter', + color: StreamChatTheme.of(context).primaryIconTheme.color, + width: 17, + ), + onTap: () async { + Navigator.pop(context); + _showEditBottomSheet(context); + }, + ); + } + + void _showEditBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showModalBottomSheet( + context: context, + elevation: 2, + clipBehavior: Clip.hardEdge, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + builder: (context) { + return StreamChannel( + channel: channel, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16.0, + left: 16.0, + right: 16.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Edit message', + style: Theme.of(context).textTheme.headline6, + ), + Container( + height: 30, + padding: const EdgeInsets.all(2.0), + child: AspectRatio( + aspectRatio: 1, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + disabledElevation: 0, + hoverElevation: 0, + onPressed: () { + Navigator.of(context).pop(); + }, + fillColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.1) + : Colors.black.withOpacity(.1), + padding: EdgeInsets.all(4), + child: Icon( + Icons.close, + size: 15, + color: StreamChatTheme.of(context) + .primaryIconTheme + .color, + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: editMessageInputBuilder != null + ? editMessageInputBuilder(context, message) + : MessageInput( + editMessage: message, + onMessageSent: (_) { + FocusScope.of(context).unfocus(); + Navigator.pop(context); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildReplyButton(BuildContext context) { + return ListTile( + title: Text( + 'Start a thread', + style: Theme.of(context).textTheme.headline6, + ), + leading: Icon( + Icons.reply, + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + onTap: () { + Navigator.pop(context); + if (onThreadTap != null) { + onThreadTap(message); + } + }, + ); + } +} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index a2020726..c8160c06 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -6,10 +6,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'image_group.dart'; -import 'message_actions_bottom_sheet.dart'; import 'message_text.dart'; typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); @@ -492,21 +492,17 @@ class _MessageWidgetState extends State { ); } - void _showMessageBottomSheet(BuildContext context) { + void _showMessageModalBottomSheet(BuildContext context) { final channel = StreamChannel.of(context).channel; - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), + showDialog( context: context, builder: (context) { return StreamChannel( channel: channel, - child: MessageActionsBottomSheet( + child: MessageActionsModal( + messageTheme: widget.messageTheme, + messageShape: widget.shape ?? _getDefaultShape(context), + reverse: widget.reverse, showDeleteMessage: widget.showDeleteMessage, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, @@ -520,6 +516,21 @@ class _MessageWidgetState extends State { }); } + ContinuousRectangleBorder _getDefaultShape(BuildContext context) { + return ContinuousRectangleBorder( + side: widget.attachmentBorderSide ?? + widget.borderSide ?? + BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ), + borderRadius: widget.attachmentBorderRadiusGeometry ?? + widget.borderRadiusGeometry ?? + BorderRadius.zero, + ); + } + List _parseAttachments(BuildContext context) { final images = widget.message.attachments .where((element) => element.type == 'image') @@ -574,18 +585,7 @@ class _MessageWidgetState extends State { clipBehavior: Clip.hardEdge, shape: widget.attachmentShape ?? widget.shape ?? - ContinuousRectangleBorder( - side: widget.attachmentBorderSide ?? - widget.borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), - borderRadius: widget.attachmentBorderRadiusGeometry ?? - widget.borderRadiusGeometry ?? - BorderRadius.zero, - ), + _getDefaultShape(context), child: Padding( padding: widget.attachmentPadding, child: ClipRRect( @@ -621,7 +621,7 @@ class _MessageWidgetState extends State { if (widget.onMessageActions != null) { widget.onMessageActions(context, widget.message); } else { - _showMessageBottomSheet(context); + _showMessageModalBottomSheet(context); } return; } diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 56127476..885d937a 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -24,7 +24,7 @@ class ReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return Material( color: Colors.black87, child: Row( crossAxisAlignment: CrossAxisAlignment.start,