Merge pull request #43 from GetStream/feature/messageInputActions

Feature/message input actions
This commit is contained in:
Salvatore Giordano
2020-05-01 15:52:51 +02:00
committed by GitHub
5 changed files with 257 additions and 170 deletions
-1
View File
@@ -67,7 +67,6 @@ void main() async {
final client = Client( final client = Client(
's2dxdhpxd94g', 's2dxdhpxd94g',
logLevel: Level.INFO, logLevel: Level.INFO,
persistenceEnabled: false,
showLocalNotification: Platform.isAndroid ? showLocalNotification : null, showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
); );
+225 -149
View File
@@ -17,6 +17,21 @@ import '../stream_chat_flutter.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
typedef FileUploader = Future<String> Function(File, Channel); typedef FileUploader = Future<String> Function(File, Channel);
typedef AttachmentThumbnailBuilder = Widget Function(
BuildContext,
_SendingAttachment,
);
enum ActionsLocation {
left,
right,
}
enum DefaultAttachmentTypes {
image,
video,
file,
}
/// Inactive state /// 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.png)
@@ -65,6 +80,7 @@ class MessageInput extends StatefulWidget {
MessageInput({ MessageInput({
Key key, Key key,
this.onMessageSent, this.onMessageSent,
this.preMessageSending,
this.parentMessage, this.parentMessage,
this.editMessage, this.editMessage,
this.maxHeight = 150, this.maxHeight = 150,
@@ -73,6 +89,10 @@ class MessageInput extends StatefulWidget {
this.doImageUploadRequest, this.doImageUploadRequest,
this.doFileUploadRequest, this.doFileUploadRequest,
this.initialMessage, this.initialMessage,
this.textEditingController,
this.actions,
this.actionsLocation = ActionsLocation.left,
this.attachmentThumbnailBuilders,
}) : super(key: key); }) : super(key: key);
/// Message to edit /// Message to edit
@@ -84,6 +104,10 @@ class MessageInput extends StatefulWidget {
/// Function called after sending the message /// Function called after sending the message
final void Function(Message) onMessageSent; final void Function(Message) onMessageSent;
/// Function called right before sending the message
/// Use this to transform the message
final FutureOr<Message> Function(Message) preMessageSending;
/// Parent message in case of a thread /// Parent message in case of a thread
final Message parentMessage; final Message parentMessage;
@@ -102,33 +126,48 @@ class MessageInput extends StatefulWidget {
/// Override file upload request /// Override file upload request
final FileUploader doFileUploadRequest; final FileUploader doFileUploadRequest;
/// The text controller of the TextField
final TextEditingController textEditingController;
/// List of action widgets
final List<Widget> actions;
/// The location of the custom actions
final ActionsLocation actionsLocation;
/// Map that defines a thumbnail builder for an attachment type
final Map<String, AttachmentThumbnailBuilder> attachmentThumbnailBuilders;
@override @override
_MessageInputState createState() => _MessageInputState( MessageInputState createState() => MessageInputState();
doFileUploadRequest: doFileUploadRequest,
doImageUploadRequest: doImageUploadRequest, /// Use this method to get the current [StreamChatState] instance
); static MessageInputState of(BuildContext context) {
MessageInputState messageInputState;
messageInputState = context.findAncestorStateOfType<MessageInputState>();
if (messageInputState == null) {
throw Exception(
'You must have a MessageInput widget as anchestor of your widget tree');
}
return messageInputState;
}
} }
class _MessageInputState extends State<MessageInput> { class MessageInputState extends State<MessageInput> {
final List<_SendingAttachment> _attachments = []; final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode(); final _focusNode = FocusNode();
final List<User> _mentionedUsers = []; final List<User> _mentionedUsers = [];
FileUploader doImageUploadRequest;
FileUploader doFileUploadRequest;
TextEditingController _textController;
bool _inputEnabled = true; bool _inputEnabled = true;
bool _messageIsPresent = false; bool _messageIsPresent = false;
bool _typingStarted = false; bool _typingStarted = false;
OverlayEntry _commandsOverlay, _mentionsOverlay; OverlayEntry _commandsOverlay, _mentionsOverlay;
_MessageInputState({ /// The editing controller passed to the input TextField
this.doImageUploadRequest, TextEditingController textEditingController;
this.doFileUploadRequest,
}) {
doImageUploadRequest ??= _uploadImage;
doFileUploadRequest ??= _uploadFile;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -165,8 +204,12 @@ class _MessageInputState extends State<MessageInput> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
if (!widget.disableAttachments) _buildAttachmentButton(), if (!widget.disableAttachments) _buildAttachmentButton(),
if (widget.actionsLocation == ActionsLocation.left)
...widget.actions ?? [],
_buildTextInput(context), _buildTextInput(context),
_animateSendButton(context), _animateSendButton(context),
if (widget.actionsLocation == ActionsLocation.right)
...widget.actions ?? [],
], ],
); );
} }
@@ -194,10 +237,10 @@ class _MessageInputState extends State<MessageInput> {
minLines: null, minLines: null,
maxLines: null, maxLines: null,
onSubmitted: (_) { onSubmitted: (_) {
_sendMessage(context); sendMessage();
}, },
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
controller: _textController, controller: textEditingController,
focusNode: _focusNode, focusNode: _focusNode,
onChanged: (s) { onChanged: (s) {
StreamChannel.of(context).channel.keyStroke(); StreamChannel.of(context).channel.keyStroke();
@@ -216,10 +259,10 @@ class _MessageInputState extends State<MessageInput> {
Overlay.of(context).insert(_commandsOverlay); Overlay.of(context).insert(_commandsOverlay);
} }
if (_textController.selection.isCollapsed && if (textEditingController.selection.isCollapsed &&
(s[_textController.selection.start - 1] == '@' || (s[textEditingController.selection.start - 1] == '@' ||
_textController.text textEditingController.text
.substring(0, _textController.selection.start) .substring(0, textEditingController.selection.start)
.split(' ') .split(' ')
.last .last
.contains('@'))) { .contains('@'))) {
@@ -279,7 +322,7 @@ class _MessageInputState extends State<MessageInput> {
} }
OverlayEntry _buildCommandsOverlayEntry() { OverlayEntry _buildCommandsOverlayEntry() {
final text = _textController.text; final text = textEditingController.text;
final commands = StreamChannel.of(context) final commands = StreamChannel.of(context)
.channel .channel
.config .config
@@ -344,8 +387,8 @@ class _MessageInputState extends State<MessageInput> {
} }
OverlayEntry _buildMentionsOverlayEntry() { OverlayEntry _buildMentionsOverlayEntry() {
final splits = _textController.text final splits = textEditingController.text
.substring(0, _textController.value.selection.start) .substring(0, textEditingController.value.selection.start)
.split('@'); .split('@');
final query = splits.last.toLowerCase(); final query = splits.last.toLowerCase();
@@ -391,10 +434,10 @@ class _MessageInputState extends State<MessageInput> {
splits[splits.length - 1] = m.user.name; splits[splits.length - 1] = m.user.name;
final rejoin = splits.join('@'); final rejoin = splits.join('@');
_textController.value = TextEditingValue( textEditingController.value = TextEditingValue(
text: rejoin + text: rejoin +
_textController.text textEditingController.text.substring(
.substring(_textController.selection.start), textEditingController.selection.start),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: rejoin.length, offset: rejoin.length,
), ),
@@ -413,7 +456,7 @@ class _MessageInputState extends State<MessageInput> {
} }
void _setCommand(Command c) { void _setCommand(Command c) {
_textController.value = TextEditingValue( textEditingController.value = TextEditingValue(
text: '/${c.name} ', text: '/${c.name} ',
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: c.name.length + 2, offset: c.name.length + 2,
@@ -452,34 +495,7 @@ class _MessageInputState extends State<MessageInput> {
width: 50, width: 50,
child: _buildAttachment(attachment), child: _buildAttachment(attachment),
), ),
Positioned( _buildRemoveButton(attachment),
height: 16,
width: 16,
top: 4,
right: 4,
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
setState(() {
_attachments.remove(attachment);
});
},
fillColor: Colors.white.withOpacity(.5),
child: Center(
child: Icon(
Icons.close,
size: 15,
),
),
),
),
attachment.uploaded attachment.uploaded
? SizedBox() ? SizedBox()
: Positioned.fill( : Positioned.fill(
@@ -499,20 +515,62 @@ class _MessageInputState extends State<MessageInput> {
); );
} }
Positioned _buildRemoveButton(_SendingAttachment attachment) {
return Positioned(
height: 16,
width: 16,
top: 4,
right: 4,
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
setState(() {
_attachments.remove(attachment);
});
},
fillColor: Colors.white.withOpacity(.5),
child: Center(
child: Icon(
Icons.close,
size: 15,
),
),
),
);
}
Widget _buildAttachment(_SendingAttachment attachment) { Widget _buildAttachment(_SendingAttachment attachment) {
switch (attachment.type) { if (widget.attachmentThumbnailBuilders
case FileType.image: ?.containsKey(attachment.attachment.type) ==
true) {
return widget.attachmentThumbnailBuilders[attachment.attachment.type](
context,
attachment,
);
}
switch (attachment.attachment.type) {
case 'image':
case 'giphy':
return attachment.file != null return attachment.file != null
? Image.file( ? Image.file(
attachment.file, attachment.file,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: Image.network( : Image.network(
attachment.url, attachment.attachment.imageUrl ??
attachment.attachment.thumbUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
); );
break; break;
case FileType.video: case 'video':
return Container( return Container(
child: Icon(Icons.videocam), child: Icon(Icons.videocam),
color: Colors.black26, color: Colors.black26,
@@ -535,7 +593,7 @@ class _MessageInputState extends State<MessageInput> {
color: Colors.transparent, color: Colors.transparent,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
_showAttachmentModal(); showAttachmentModal();
}, },
icon: Icon( icon: Icon(
Icons.add_circle_outline, Icons.add_circle_outline,
@@ -544,7 +602,8 @@ class _MessageInputState extends State<MessageInput> {
); );
} }
void _showAttachmentModal() { /// Show the attachment modal, making the user choose where to pick a media from
void showAttachmentModal() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_focusNode.unfocus(); _focusNode.unfocus();
} }
@@ -575,7 +634,7 @@ class _MessageInputState extends State<MessageInput> {
leading: Icon(Icons.image), leading: Icon(Icons.image),
title: Text('Upload a photo'), title: Text('Upload a photo'),
onTap: () { onTap: () {
_pickFile(FileType.image, false); pickFile(DefaultAttachmentTypes.image, false);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -583,7 +642,7 @@ class _MessageInputState extends State<MessageInput> {
leading: Icon(Icons.video_library), leading: Icon(Icons.video_library),
title: Text('Upload a video'), title: Text('Upload a video'),
onTap: () { onTap: () {
_pickFile(FileType.video, false); pickFile(DefaultAttachmentTypes.video, false);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -591,7 +650,7 @@ class _MessageInputState extends State<MessageInput> {
leading: Icon(Icons.camera_alt), leading: Icon(Icons.camera_alt),
title: Text('Photo from camera'), title: Text('Photo from camera'),
onTap: () { onTap: () {
_pickFile(FileType.image, true); pickFile(DefaultAttachmentTypes.image, true);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -599,7 +658,7 @@ class _MessageInputState extends State<MessageInput> {
leading: Icon(Icons.videocam), leading: Icon(Icons.videocam),
title: Text('Video from camera'), title: Text('Video from camera'),
onTap: () { onTap: () {
_pickFile(FileType.video, true); pickFile(DefaultAttachmentTypes.video, true);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -607,7 +666,7 @@ class _MessageInputState extends State<MessageInput> {
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
title: Text('Upload a file'), title: Text('Upload a file'),
onTap: () { onTap: () {
_pickFile(FileType.any, false); pickFile(DefaultAttachmentTypes.file, false);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
@@ -616,20 +675,50 @@ class _MessageInputState extends State<MessageInput> {
}); });
} }
void _pickFile(FileType type, bool camera) async { /// Add an attachment to the sending message
/// Use this to add custom type attachments
void addAttachment(Attachment attachment) {
setState(() {
_attachments.add(_SendingAttachment(
attachment: attachment,
uploaded: true,
));
});
}
/// Pick a file from the device
/// If [camera] is true then the camera will open
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
setState(() { setState(() {
_inputEnabled = false; _inputEnabled = false;
}); });
File file; File file;
String attachmentType;
if (fileType == DefaultAttachmentTypes.image) {
attachmentType = 'image';
} else if (fileType == DefaultAttachmentTypes.video) {
attachmentType = 'video';
} else if (fileType == DefaultAttachmentTypes.file) {
attachmentType = 'file';
}
if (camera) { if (camera) {
if (type == FileType.image) { if (fileType == DefaultAttachmentTypes.image) {
file = await ImagePicker.pickImage(source: ImageSource.camera); file = await ImagePicker.pickImage(source: ImageSource.camera);
} else if (type == FileType.video) { } else if (fileType == DefaultAttachmentTypes.video) {
file = await ImagePicker.pickVideo(source: ImageSource.camera); file = await ImagePicker.pickVideo(source: ImageSource.camera);
} }
} else { } else {
FileType type;
if (fileType == DefaultAttachmentTypes.image) {
type = FileType.image;
} else if (fileType == DefaultAttachmentTypes.video) {
type = FileType.video;
} else if (fileType == DefaultAttachmentTypes.file) {
type = FileType.any;
}
file = await FilePicker.getFile(type: type); file = await FilePicker.getFile(type: type);
} }
@@ -645,16 +734,27 @@ class _MessageInputState extends State<MessageInput> {
final attachment = _SendingAttachment( final attachment = _SendingAttachment(
file: file, file: file,
type: type, attachment: Attachment(
localUri: file.uri,
type: attachmentType,
),
); );
setState(() { setState(() {
_attachments.add(attachment); _attachments.add(attachment);
}); });
final url = await _uploadAttachment(file, type, channel); final url = await _uploadAttachment(file, fileType, channel);
attachment.url = url; if (fileType == DefaultAttachmentTypes.image) {
attachment.attachment = attachment.attachment.copyWith(
imageUrl: url,
);
} else {
attachment.attachment = attachment.attachment.copyWith(
assetUrl: url,
);
}
setState(() { setState(() {
attachment.uploaded = true; attachment.uploaded = true;
@@ -663,14 +763,22 @@ class _MessageInputState extends State<MessageInput> {
Future<String> _uploadAttachment( Future<String> _uploadAttachment(
File file, File file,
FileType type, DefaultAttachmentTypes type,
Channel channel, Channel channel,
) async { ) async {
String url; String url;
if (type == FileType.image) { if (type == DefaultAttachmentTypes.image) {
url = await doImageUploadRequest(file, channel); if (widget.doImageUploadRequest != null) {
url = await widget.doImageUploadRequest(file, channel);
} else {
url = await _uploadImage(file, channel);
}
} else { } else {
url = await doFileUploadRequest(file, channel); if (widget.doFileUploadRequest != null) {
url = await widget.doFileUploadRequest(file, channel);
} else {
url = await _uploadFile(file, channel);
}
} }
return url; return url;
} }
@@ -714,7 +822,7 @@ class _MessageInputState extends State<MessageInput> {
child: IconButton( child: IconButton(
key: Key('sendButton'), key: Key('sendButton'),
onPressed: () { onPressed: () {
_sendMessage(context); sendMessage();
}, },
icon: Icon( icon: Icon(
Icons.send, Icons.send,
@@ -725,15 +833,16 @@ class _MessageInputState extends State<MessageInput> {
); );
} }
void _sendMessage(BuildContext context) { /// Sends the current message
final text = _textController.text.trim(); void sendMessage() async {
final text = textEditingController.text.trim();
if (text.isEmpty && _attachments.isEmpty) { if (text.isEmpty && _attachments.isEmpty) {
return; return;
} }
final attachments = List<_SendingAttachment>.from(_attachments); final attachments = List<_SendingAttachment>.from(_attachments);
_textController.clear(); textEditingController.clear();
_attachments.clear(); _attachments.clear();
setState(() { setState(() {
@@ -754,22 +863,10 @@ class _MessageInputState extends State<MessageInput> {
message = widget.editMessage.copyWith( message = widget.editMessage.copyWith(
parentId: widget.parentMessage?.id, parentId: widget.parentMessage?.id,
text: text, text: text,
attachments: widget.editMessage.attachments attachments: _getAttachments(attachments).toList(),
.where((attachment) => attachment.type == 'giphy')
.toList() +
_getAttachments(attachments).toList(),
mentionedUsers: mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
); );
if (widget.editMessage.status == MessageSendingStatus.FAILED) {
sendingFuture = channel.sendMessage(message);
}
sendingFuture = StreamChat.of(context).client.updateMessage(
message,
channel.cid,
);
} else { } else {
message = (widget.initialMessage ?? Message()).copyWith( message = (widget.initialMessage ?? Message()).copyWith(
parentId: widget.parentMessage?.id, parentId: widget.parentMessage?.id,
@@ -778,35 +875,36 @@ class _MessageInputState extends State<MessageInput> {
mentionedUsers: mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
); );
sendingFuture = channel.sendMessage(message);
} }
sendingFuture.whenComplete(() { if (widget.preMessageSending != null) {
message = await widget.preMessageSending(message);
}
if (widget.editMessage == null ||
widget.editMessage.status == MessageSendingStatus.FAILED) {
sendingFuture = channel.sendMessage(message);
} else {
sendingFuture = StreamChat.of(context).client.updateMessage(
message,
channel.cid,
);
}
return sendingFuture.whenComplete(() {
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
widget.onMessageSent(message); widget.onMessageSent(message);
} else {
if (widget.editMessage != null) {
Navigator.pop(context);
}
} }
}); });
} }
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) { Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
return attachments.map((attachment) { return attachments.map((attachment) {
String type; return attachment.attachment;
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,
localUri: attachment.file.uri,
);
}); });
} }
@@ -821,7 +919,7 @@ class _MessageInputState extends State<MessageInput> {
_keyboardListener = KeyboardVisibility.onChange.listen((visible) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) {
if (visible) { if (visible) {
if (_commandsOverlay != null) { if (_commandsOverlay != null) {
if (_textController.text.startsWith('/')) { if (textEditingController.text.startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildCommandsOverlayEntry(); _commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay); Overlay.of(context).insert(_commandsOverlay);
@@ -830,7 +928,7 @@ class _MessageInputState extends State<MessageInput> {
} }
if (_mentionsOverlay != null) { if (_mentionsOverlay != null) {
if (_textController.text.contains('@')) { if (textEditingController.text.contains('@')) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_mentionsOverlay = _buildCommandsOverlayEntry(); _mentionsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay); Overlay.of(context).insert(_mentionsOverlay);
@@ -847,44 +945,24 @@ class _MessageInputState extends State<MessageInput> {
} }
}); });
if (widget.editMessage != null) { textEditingController =
_parseExistingMessage(widget.editMessage); widget.textEditingController ?? TextEditingController();
} else if (widget.initialMessage != null) { if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.initialMessage); _parseExistingMessage(widget.editMessage ?? widget.initialMessage);
} else {
_textController = TextEditingController();
} }
} }
void _parseExistingMessage(Message message) { void _parseExistingMessage(Message message) {
_textController = TextEditingController(text: message.text); textEditingController.text = message.text;
_typingStarted = true; _typingStarted = true;
_messageIsPresent = true; _messageIsPresent = true;
message.attachments?.forEach((attachment) { message.attachments?.forEach((attachment) {
if (attachment.type == 'image') { _attachments.add(_SendingAttachment(
_attachments.add(_SendingAttachment( attachment: attachment,
type: FileType.image, uploaded: true,
url: attachment.imageUrl ?? ));
attachment.assetUrl ??
attachment.thumbUrl ??
attachment.ogScrapeUrl,
uploaded: true,
));
} else if (attachment.type == 'video') {
_attachments.add(_SendingAttachment(
type: FileType.video,
url: attachment.assetUrl,
uploaded: true,
));
} else if (attachment.type != 'giphy') {
_attachments.add(_SendingAttachment(
type: FileType.any,
url: attachment.assetUrl,
uploaded: true,
));
}
}); });
} }
@@ -908,15 +986,13 @@ class _MessageInputState extends State<MessageInput> {
} }
class _SendingAttachment { class _SendingAttachment {
final File file; File file;
final FileType type; Attachment attachment;
String url;
bool uploaded; bool uploaded;
_SendingAttachment({ _SendingAttachment({
this.url,
this.file, this.file,
this.type, this.attachment,
this.uploaded = false, this.uploaded = false,
}); });
} }
+10
View File
@@ -73,6 +73,7 @@ class MessageListView extends StatefulWidget {
this.attachmentBuilders, this.attachmentBuilders,
this.dateDividerBuilder, this.dateDividerBuilder,
this.showAvatar = true, this.showAvatar = true,
this.editMessageInputBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(), this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key); }) : super(key: key);
@@ -116,9 +117,14 @@ class MessageListView extends StatefulWidget {
/// if true shows the user avatar /// if true shows the user avatar
final bool showAvatar; final bool showAvatar;
/// Builder used to build the message input to edit a message
final Widget Function(BuildContext, Message) editMessageInputBuilder;
/// The ScrollPhysics used by the ListView /// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics; final ScrollPhysics scrollPhysics;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -182,6 +188,7 @@ class _MessageListViewState extends State<MessageListView> {
onMessageActions: widget.onMessageActions, onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
@@ -248,6 +255,7 @@ class _MessageListViewState extends State<MessageListView> {
onMessageActions: widget.onMessageActions, onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
); );
} }
} }
@@ -342,6 +350,7 @@ class _MessageListViewState extends State<MessageListView> {
onMessageActions: widget.onMessageActions, onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
); );
} }
@@ -384,6 +393,7 @@ class _MessageListViewState extends State<MessageListView> {
onMessageActions: widget.onMessageActions, onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
); );
} }
+21 -19
View File
@@ -51,6 +51,7 @@ class MessageWidget extends StatefulWidget {
this.showVideoFullScreen = true, this.showVideoFullScreen = true,
this.attachmentBuilders, this.attachmentBuilders,
this.showAvatar = true, this.showAvatar = true,
this.editMessageInputBuilder,
}) : super(key: key); }) : super(key: key);
/// Function called on mention tap /// Function called on mention tap
@@ -89,6 +90,9 @@ class MessageWidget extends StatefulWidget {
/// if true shows the user avatar /// if true shows the user avatar
final bool showAvatar; final bool showAvatar;
/// Builder used to build the message input to edit a message
final Widget Function(BuildContext, Message) editMessageInputBuilder;
@override @override
_MessageWidgetState createState() => _MessageWidgetState(); _MessageWidgetState createState() => _MessageWidgetState();
} }
@@ -712,10 +716,6 @@ class _MessageWidgetState extends State<MessageWidget>
child: Icon( child: Icon(
Icons.close, Icons.close,
size: 15, size: 15,
color:
Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
), ),
), ),
), ),
@@ -727,21 +727,23 @@ class _MessageWidgetState extends State<MessageWidget>
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: MessageInput( child: widget.editMessageInputBuilder != null
editMessage: widget.message, ? widget.editMessageInputBuilder(context, widget.message)
parentMessage: widget.isParent : MessageInput(
? StreamChannel.of(context) editMessage: widget.message,
.channel parentMessage: widget.isParent
.state ? StreamChannel.of(context)
.messages .channel
.firstWhere((message) => .state
message.id == widget.message.parentId) .messages
: null, .firstWhere((message) =>
onMessageSent: (_) { message.id == widget.message.parentId)
FocusScope.of(context).unfocus(); : null,
Navigator.pop(context); onMessageSent: (_) {
}, FocusScope.of(context).unfocus();
), Navigator.pop(context);
},
),
), ),
], ],
), ),
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies:
file_picker: ^1.6.3+2 file_picker: ^1.6.3+2
image_picker: ^0.6.5 image_picker: ^0.6.5
flutter_keyboard_visibility: ^2.0.0 flutter_keyboard_visibility: ^2.0.0
stream_chat: ^0.2.0-alpha+7 stream_chat: ^0.2.0-alpha+8
mime: ^0.9.6+3 mime: ^0.9.6+3
visibility_detector: ^0.1.4 visibility_detector: ^0.1.4
http_parser: ^3.1.4 http_parser: ^3.1.4