From f3a6ba68a406d898579d25adbfbbbc15178ba2b1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 3 Mar 2020 11:03:36 +0100 Subject: [PATCH] add edit message feature --- example/lib/main.dart | 14 ++-- lib/src/message_input.dart | 151 ++++++++++++++++++++++++++---------- lib/src/message_widget.dart | 108 +++++++++++++++++++++++++- pubspec.yaml | 2 +- 4 files changed, 227 insertions(+), 48 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a28a4320..15efe5e5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,13 +3,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() async { final client = Client( - 'b67pax5b2wdq', + 's2dxdhpxd94g', logLevel: Level.INFO, ); await client.setUser( User(id: 'falling-mountain-7'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E', + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNDE5MjI5YTMtODRhMC00MDZiLTkzNzEtN2NlOWE0ZTBhNjc2In0.J3SjGH4e4v7b3cg5EgWkljTxXj_HeHpCWn5ujEVv_H8', ); runApp(MyApp(client)); @@ -38,11 +38,11 @@ class ChannelListPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: ChannelListView( - filter: { - 'members': { - '\$in': [StreamChat.of(context).user.id], - } - }, +// filter: { +// 'members': { +// '\$in': [StreamChat.of(context).user.id], +// } +// }, sort: [SortOption('last_message_at')], pagination: PaginationParams( limit: 20, diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6db8a574..eb95d6d5 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -8,10 +8,13 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; +/// Inactive state /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) +/// Focused state /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2_paint.png) /// @@ -55,8 +58,12 @@ class MessageInput extends StatefulWidget { Key key, this.onMessageSent, this.parentMessage, + this.editMessage, }) : super(key: key); + /// Message to edit + final Message editMessage; + /// Function called after sending the message final void Function(Message) onMessageSent; @@ -68,11 +75,13 @@ class MessageInput extends StatefulWidget { } class _MessageInputState extends State { - final _textController = TextEditingController(); + TextEditingController _textController; bool _messageIsPresent = false; bool _typingStarted = false; final List<_SendingAttachment> _attachments = []; + final _focusNode = FocusNode(); + @override Widget build(BuildContext context) { return SafeArea( @@ -87,9 +96,7 @@ class _MessageInputState extends State { padding: EdgeInsets.all(2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), - gradient: _typingStarted - ? StreamChatTheme.of(context).channelTheme.inputGradient - : null, + gradient: _getGradient(context), ), child: Container( decoration: BoxDecoration( @@ -127,6 +134,7 @@ class _MessageInputState extends State { _sendMessage(context); }, controller: _textController, + focusNode: _focusNode, onChanged: (s) { StreamChannel.of(context).channel.keyStroke(); setState(() { @@ -167,6 +175,19 @@ class _MessageInputState extends State { ); } + Gradient _getGradient(BuildContext context) { + if (_typingStarted) { + if (widget.editMessage == null) { + return StreamChatTheme.of(context).channelTheme.inputGradient; + } + return LinearGradient( + colors: [Colors.lightGreen, Colors.green], + ); + } else { + return null; + } + } + Widget _buildAttachments() { return Wrap( direction: Axis.horizontal, @@ -234,10 +255,15 @@ class _MessageInputState extends State { Widget _buildAttachment(_SendingAttachment attachment) { switch (attachment.type) { case FileType.IMAGE: - return Image.file( - attachment.file, - fit: BoxFit.cover, - ); + return attachment.file != null + ? Image.file( + attachment.file, + fit: BoxFit.cover, + ) + : Image.network( + attachment.url, + fit: BoxFit.cover, + ); break; case FileType.VIDEO: return Container( @@ -414,50 +440,97 @@ class _MessageInputState extends State { }); FocusScope.of(context).unfocus(); - StreamChannel.of(context) - .channel - .sendMessage( - Message( - parentId: widget.parentMessage?.id, - text: text, - attachments: attachments.map((attachment) { - String type; - switch (attachment.type) { - case FileType.IMAGE: - type = 'image'; - break; - case FileType.VIDEO: - type = 'video'; - break; - default: - type = 'file'; - } - return Attachment( - imageUrl: - attachment.type == FileType.IMAGE ? attachment.url : null, - assetUrl: attachment.url, - type: type, - ); - }).toList(), - ), - ) - .then((_) { - if (widget.onMessageSent != null) { - widget.onMessageSent(Message(text: text)); + if (widget.editMessage != null) { + final message = widget.editMessage.copyWith( + parentId: widget.parentMessage?.id, + text: text, + attachments: _getAttachments(attachments).toList(), + ); + StreamChat.of(context).client.updateMessage(message).then((_) { + if (widget.onMessageSent != null) { + widget.onMessageSent(message); + } + }); + } else { + final message = Message( + parentId: widget.parentMessage?.id, + text: text, + attachments: _getAttachments(attachments).toList(), + ); + StreamChannel.of(context).channel.sendMessage(message).then((_) { + if (widget.onMessageSent != null) { + widget.onMessageSent(message); + } + }); + } + } + + Iterable _getAttachments(List<_SendingAttachment> attachments) { + return attachments.map((attachment) { + String type; + switch (attachment.type) { + case FileType.IMAGE: + type = 'image'; + break; + case FileType.VIDEO: + type = 'video'; + break; + default: + type = 'file'; } + return Attachment( + imageUrl: attachment.type == FileType.IMAGE ? attachment.url : null, + assetUrl: attachment.url, + type: type, + ); }); } + + @override + void initState() { + super.initState(); + + if (widget.editMessage != null) { + _textController = TextEditingController(text: widget.editMessage.text); + + _typingStarted = true; + _messageIsPresent = true; + + widget.editMessage.attachments.forEach((attachment) { + if (attachment.type == 'image') { + _attachments.add(_SendingAttachment( + type: FileType.IMAGE, + url: attachment.imageUrl, + uploaded: true, + )); + } + }); + } else { + _textController = TextEditingController(); + } + } + + bool _focused = false; + @override + void didChangeDependencies() { + if (widget.editMessage != null && !_focused) { + FocusScope.of(context).requestFocus(_focusNode); + _focused = true; + } + super.didChangeDependencies(); + } } class _SendingAttachment { final File file; final FileType type; String url; - bool uploaded = false; + bool uploaded; _SendingAttachment({ this.url, this.file, this.type, + this.uploaded = false, }); } diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 7cb737ad..2570bdad 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_channel.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -34,8 +35,12 @@ class MessageWidget extends StatefulWidget { @required this.nextMessage, this.onThreadTap, this.isParent = false, + this.onMessageEdit, }) : super(key: key); + /// Function called when editing a message + final Function(Message) onMessageEdit; + /// This message final Message message; @@ -399,7 +404,7 @@ class _MessageWidgetState extends State ), ), context: context, - builder: (context) { + builder: (_) { final fontSize = 30.0; final textStyle = TextStyle( fontSize: fontSize, @@ -478,6 +483,22 @@ class _MessageWidgetState extends State }, ) : SizedBox(), + isMyMessage + ? FlatButton( + child: Padding( + padding: const EdgeInsets.all(28.0), + child: Text( + 'Edit message', + style: Theme.of(context).textTheme.headline, + ), + ), + onPressed: () async { + await Navigator.pop(context); + + _showEditBottomSheet(context); + }, + ) + : SizedBox(), streamChannel.channel.config.replies ? FlatButton( child: Padding( @@ -499,6 +520,91 @@ class _MessageWidgetState extends State }); } + void _showEditBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + elevation: 2, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + builder: (context) { + return 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.title, + ), + 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: Colors.black.withOpacity(.1), + padding: EdgeInsets.all(4), + child: Icon( + Icons.close, + size: 15, + color: Colors.black, + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: MessageInput( + editMessage: widget.message, + parentMessage: widget.isParent + ? StreamChannel.of(context) + .channel + .state + .messages + .firstWhere( + (message) => message.id == widget.message.parentId) + : null, + onMessageSent: (_) { + Navigator.pop(context); + }, + ), + ), + ], + ); + }, + ); + } + Widget _buildReactions(bool isMyMessage) { return GestureDetector( onTap: () { diff --git a/pubspec.yaml b/pubspec.yaml index 3662c3ce..491a1663 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: chewie: ^0.9.8+1 file_picker: ^1.4.3+2 image_picker: ^0.6.3+4 - stream_chat: ^0.1.12 + stream_chat: ^0.1.13 dev_dependencies: pedantic: ^1.8.0+1