Merge pull request #43 from GetStream/feature/messageInputActions
Feature/message input actions
This commit is contained in:
@@ -67,7 +67,6 @@ void main() async {
|
||||
final client = Client(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
persistenceEnabled: false,
|
||||
showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
|
||||
);
|
||||
|
||||
|
||||
+225
-149
@@ -17,6 +17,21 @@ import '../stream_chat_flutter.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
typedef FileUploader = Future<String> Function(File, Channel);
|
||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
_SendingAttachment,
|
||||
);
|
||||
|
||||
enum ActionsLocation {
|
||||
left,
|
||||
right,
|
||||
}
|
||||
|
||||
enum DefaultAttachmentTypes {
|
||||
image,
|
||||
video,
|
||||
file,
|
||||
}
|
||||
|
||||
/// Inactive state
|
||||
/// 
|
||||
@@ -65,6 +80,7 @@ class MessageInput extends StatefulWidget {
|
||||
MessageInput({
|
||||
Key key,
|
||||
this.onMessageSent,
|
||||
this.preMessageSending,
|
||||
this.parentMessage,
|
||||
this.editMessage,
|
||||
this.maxHeight = 150,
|
||||
@@ -73,6 +89,10 @@ class MessageInput extends StatefulWidget {
|
||||
this.doImageUploadRequest,
|
||||
this.doFileUploadRequest,
|
||||
this.initialMessage,
|
||||
this.textEditingController,
|
||||
this.actions,
|
||||
this.actionsLocation = ActionsLocation.left,
|
||||
this.attachmentThumbnailBuilders,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
@@ -84,6 +104,10 @@ class MessageInput extends StatefulWidget {
|
||||
/// Function called after sending the message
|
||||
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
|
||||
final Message parentMessage;
|
||||
|
||||
@@ -102,33 +126,48 @@ class MessageInput extends StatefulWidget {
|
||||
/// Override file upload request
|
||||
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
|
||||
_MessageInputState createState() => _MessageInputState(
|
||||
doFileUploadRequest: doFileUploadRequest,
|
||||
doImageUploadRequest: doImageUploadRequest,
|
||||
);
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
/// 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 _focusNode = FocusNode();
|
||||
final List<User> _mentionedUsers = [];
|
||||
FileUploader doImageUploadRequest;
|
||||
FileUploader doFileUploadRequest;
|
||||
|
||||
TextEditingController _textController;
|
||||
bool _inputEnabled = true;
|
||||
bool _messageIsPresent = false;
|
||||
bool _typingStarted = false;
|
||||
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
||||
|
||||
_MessageInputState({
|
||||
this.doImageUploadRequest,
|
||||
this.doFileUploadRequest,
|
||||
}) {
|
||||
doImageUploadRequest ??= _uploadImage;
|
||||
doFileUploadRequest ??= _uploadFile;
|
||||
}
|
||||
/// The editing controller passed to the input TextField
|
||||
TextEditingController textEditingController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -165,8 +204,12 @@ class _MessageInputState extends State<MessageInput> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||
if (widget.actionsLocation == ActionsLocation.left)
|
||||
...widget.actions ?? [],
|
||||
_buildTextInput(context),
|
||||
_animateSendButton(context),
|
||||
if (widget.actionsLocation == ActionsLocation.right)
|
||||
...widget.actions ?? [],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -194,10 +237,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
onSubmitted: (_) {
|
||||
_sendMessage(context);
|
||||
sendMessage();
|
||||
},
|
||||
keyboardType: widget.keyboardType,
|
||||
controller: _textController,
|
||||
controller: textEditingController,
|
||||
focusNode: _focusNode,
|
||||
onChanged: (s) {
|
||||
StreamChannel.of(context).channel.keyStroke();
|
||||
@@ -216,10 +259,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
}
|
||||
|
||||
if (_textController.selection.isCollapsed &&
|
||||
(s[_textController.selection.start - 1] == '@' ||
|
||||
_textController.text
|
||||
.substring(0, _textController.selection.start)
|
||||
if (textEditingController.selection.isCollapsed &&
|
||||
(s[textEditingController.selection.start - 1] == '@' ||
|
||||
textEditingController.text
|
||||
.substring(0, textEditingController.selection.start)
|
||||
.split(' ')
|
||||
.last
|
||||
.contains('@'))) {
|
||||
@@ -279,7 +322,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
OverlayEntry _buildCommandsOverlayEntry() {
|
||||
final text = _textController.text;
|
||||
final text = textEditingController.text;
|
||||
final commands = StreamChannel.of(context)
|
||||
.channel
|
||||
.config
|
||||
@@ -344,8 +387,8 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
OverlayEntry _buildMentionsOverlayEntry() {
|
||||
final splits = _textController.text
|
||||
.substring(0, _textController.value.selection.start)
|
||||
final splits = textEditingController.text
|
||||
.substring(0, textEditingController.value.selection.start)
|
||||
.split('@');
|
||||
final query = splits.last.toLowerCase();
|
||||
|
||||
@@ -391,10 +434,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||
splits[splits.length - 1] = m.user.name;
|
||||
final rejoin = splits.join('@');
|
||||
|
||||
_textController.value = TextEditingValue(
|
||||
textEditingController.value = TextEditingValue(
|
||||
text: rejoin +
|
||||
_textController.text
|
||||
.substring(_textController.selection.start),
|
||||
textEditingController.text.substring(
|
||||
textEditingController.selection.start),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: rejoin.length,
|
||||
),
|
||||
@@ -413,7 +456,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
void _setCommand(Command c) {
|
||||
_textController.value = TextEditingValue(
|
||||
textEditingController.value = TextEditingValue(
|
||||
text: '/${c.name} ',
|
||||
selection: TextSelection.collapsed(
|
||||
offset: c.name.length + 2,
|
||||
@@ -452,34 +495,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
width: 50,
|
||||
child: _buildAttachment(attachment),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildRemoveButton(attachment),
|
||||
attachment.uploaded
|
||||
? SizedBox()
|
||||
: 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) {
|
||||
switch (attachment.type) {
|
||||
case FileType.image:
|
||||
if (widget.attachmentThumbnailBuilders
|
||||
?.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
|
||||
? Image.file(
|
||||
attachment.file,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.network(
|
||||
attachment.url,
|
||||
attachment.attachment.imageUrl ??
|
||||
attachment.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
break;
|
||||
case FileType.video:
|
||||
case 'video':
|
||||
return Container(
|
||||
child: Icon(Icons.videocam),
|
||||
color: Colors.black26,
|
||||
@@ -535,7 +593,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_showAttachmentModal();
|
||||
showAttachmentModal();
|
||||
},
|
||||
icon: Icon(
|
||||
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) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
@@ -575,7 +634,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
leading: Icon(Icons.image),
|
||||
title: Text('Upload a photo'),
|
||||
onTap: () {
|
||||
_pickFile(FileType.image, false);
|
||||
pickFile(DefaultAttachmentTypes.image, false);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
@@ -583,7 +642,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
leading: Icon(Icons.video_library),
|
||||
title: Text('Upload a video'),
|
||||
onTap: () {
|
||||
_pickFile(FileType.video, false);
|
||||
pickFile(DefaultAttachmentTypes.video, false);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
@@ -591,7 +650,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
leading: Icon(Icons.camera_alt),
|
||||
title: Text('Photo from camera'),
|
||||
onTap: () {
|
||||
_pickFile(FileType.image, true);
|
||||
pickFile(DefaultAttachmentTypes.image, true);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
@@ -599,7 +658,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
leading: Icon(Icons.videocam),
|
||||
title: Text('Video from camera'),
|
||||
onTap: () {
|
||||
_pickFile(FileType.video, true);
|
||||
pickFile(DefaultAttachmentTypes.video, true);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
@@ -607,7 +666,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
title: Text('Upload a file'),
|
||||
onTap: () {
|
||||
_pickFile(FileType.any, false);
|
||||
pickFile(DefaultAttachmentTypes.file, false);
|
||||
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(() {
|
||||
_inputEnabled = false;
|
||||
});
|
||||
|
||||
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 (type == FileType.image) {
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
file = await ImagePicker.pickImage(source: ImageSource.camera);
|
||||
} else if (type == FileType.video) {
|
||||
} else if (fileType == DefaultAttachmentTypes.video) {
|
||||
file = await ImagePicker.pickVideo(source: ImageSource.camera);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -645,16 +734,27 @@ class _MessageInputState extends State<MessageInput> {
|
||||
|
||||
final attachment = _SendingAttachment(
|
||||
file: file,
|
||||
type: type,
|
||||
attachment: Attachment(
|
||||
localUri: file.uri,
|
||||
type: attachmentType,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_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(() {
|
||||
attachment.uploaded = true;
|
||||
@@ -663,14 +763,22 @@ class _MessageInputState extends State<MessageInput> {
|
||||
|
||||
Future<String> _uploadAttachment(
|
||||
File file,
|
||||
FileType type,
|
||||
DefaultAttachmentTypes type,
|
||||
Channel channel,
|
||||
) async {
|
||||
String url;
|
||||
if (type == FileType.image) {
|
||||
url = await doImageUploadRequest(file, channel);
|
||||
if (type == DefaultAttachmentTypes.image) {
|
||||
if (widget.doImageUploadRequest != null) {
|
||||
url = await widget.doImageUploadRequest(file, channel);
|
||||
} else {
|
||||
url = await _uploadImage(file, channel);
|
||||
}
|
||||
} else {
|
||||
url = await doFileUploadRequest(file, channel);
|
||||
if (widget.doFileUploadRequest != null) {
|
||||
url = await widget.doFileUploadRequest(file, channel);
|
||||
} else {
|
||||
url = await _uploadFile(file, channel);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -714,7 +822,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
child: IconButton(
|
||||
key: Key('sendButton'),
|
||||
onPressed: () {
|
||||
_sendMessage(context);
|
||||
sendMessage();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
@@ -725,15 +833,16 @@ class _MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(BuildContext context) {
|
||||
final text = _textController.text.trim();
|
||||
/// Sends the current message
|
||||
void sendMessage() async {
|
||||
final text = textEditingController.text.trim();
|
||||
if (text.isEmpty && _attachments.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final attachments = List<_SendingAttachment>.from(_attachments);
|
||||
|
||||
_textController.clear();
|
||||
textEditingController.clear();
|
||||
_attachments.clear();
|
||||
|
||||
setState(() {
|
||||
@@ -754,22 +863,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||
message = widget.editMessage.copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
text: text,
|
||||
attachments: widget.editMessage.attachments
|
||||
.where((attachment) => attachment.type == 'giphy')
|
||||
.toList() +
|
||||
_getAttachments(attachments).toList(),
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
mentionedUsers:
|
||||
_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 {
|
||||
message = (widget.initialMessage ?? Message()).copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
@@ -778,35 +875,36 @@ class _MessageInputState extends State<MessageInput> {
|
||||
mentionedUsers:
|
||||
_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) {
|
||||
widget.onMessageSent(message);
|
||||
} else {
|
||||
if (widget.editMessage != null) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Iterable<Attachment> _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,
|
||||
localUri: attachment.file.uri,
|
||||
);
|
||||
return attachment.attachment;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -821,7 +919,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
||||
if (visible) {
|
||||
if (_commandsOverlay != null) {
|
||||
if (_textController.text.startsWith('/')) {
|
||||
if (textEditingController.text.startsWith('/')) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
@@ -830,7 +928,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
if (_mentionsOverlay != null) {
|
||||
if (_textController.text.contains('@')) {
|
||||
if (textEditingController.text.contains('@')) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_mentionsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_mentionsOverlay);
|
||||
@@ -847,44 +945,24 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.editMessage != null) {
|
||||
_parseExistingMessage(widget.editMessage);
|
||||
} else if (widget.initialMessage != null) {
|
||||
_parseExistingMessage(widget.initialMessage);
|
||||
} else {
|
||||
_textController = TextEditingController();
|
||||
textEditingController =
|
||||
widget.textEditingController ?? TextEditingController();
|
||||
if (widget.editMessage != null || widget.initialMessage != null) {
|
||||
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void _parseExistingMessage(Message message) {
|
||||
_textController = TextEditingController(text: message.text);
|
||||
textEditingController.text = message.text;
|
||||
|
||||
_typingStarted = true;
|
||||
_messageIsPresent = true;
|
||||
|
||||
message.attachments?.forEach((attachment) {
|
||||
if (attachment.type == 'image') {
|
||||
_attachments.add(_SendingAttachment(
|
||||
type: FileType.image,
|
||||
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,
|
||||
));
|
||||
}
|
||||
_attachments.add(_SendingAttachment(
|
||||
attachment: attachment,
|
||||
uploaded: true,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -908,15 +986,13 @@ class _MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
class _SendingAttachment {
|
||||
final File file;
|
||||
final FileType type;
|
||||
String url;
|
||||
File file;
|
||||
Attachment attachment;
|
||||
bool uploaded;
|
||||
|
||||
_SendingAttachment({
|
||||
this.url,
|
||||
this.file,
|
||||
this.type,
|
||||
this.attachment,
|
||||
this.uploaded = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class MessageListView extends StatefulWidget {
|
||||
this.attachmentBuilders,
|
||||
this.dateDividerBuilder,
|
||||
this.showAvatar = true,
|
||||
this.editMessageInputBuilder,
|
||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -116,9 +117,14 @@ class MessageListView extends StatefulWidget {
|
||||
/// if true shows the user avatar
|
||||
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
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -182,6 +188,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onMessageActions: widget.onMessageActions,
|
||||
attachmentBuilders: widget.attachmentBuilders,
|
||||
showAvatar: widget.showAvatar,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -248,6 +255,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onMessageActions: widget.onMessageActions,
|
||||
attachmentBuilders: widget.attachmentBuilders,
|
||||
showAvatar: widget.showAvatar,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -342,6 +350,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onMessageActions: widget.onMessageActions,
|
||||
attachmentBuilders: widget.attachmentBuilders,
|
||||
showAvatar: widget.showAvatar,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,6 +393,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onMessageActions: widget.onMessageActions,
|
||||
attachmentBuilders: widget.attachmentBuilders,
|
||||
showAvatar: widget.showAvatar,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+21
-19
@@ -51,6 +51,7 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showVideoFullScreen = true,
|
||||
this.attachmentBuilders,
|
||||
this.showAvatar = true,
|
||||
this.editMessageInputBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function called on mention tap
|
||||
@@ -89,6 +90,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// if true shows the user avatar
|
||||
final bool showAvatar;
|
||||
|
||||
/// Builder used to build the message input to edit a message
|
||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||
|
||||
@override
|
||||
_MessageWidgetState createState() => _MessageWidgetState();
|
||||
}
|
||||
@@ -712,10 +716,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
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(
|
||||
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: (_) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
child: widget.editMessageInputBuilder != null
|
||||
? widget.editMessageInputBuilder(context, widget.message)
|
||||
: MessageInput(
|
||||
editMessage: widget.message,
|
||||
parentMessage: widget.isParent
|
||||
? StreamChannel.of(context)
|
||||
.channel
|
||||
.state
|
||||
.messages
|
||||
.firstWhere((message) =>
|
||||
message.id == widget.message.parentId)
|
||||
: null,
|
||||
onMessageSent: (_) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ dependencies:
|
||||
file_picker: ^1.6.3+2
|
||||
image_picker: ^0.6.5
|
||||
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
|
||||
visibility_detector: ^0.1.4
|
||||
http_parser: ^3.1.4
|
||||
|
||||
Reference in New Issue
Block a user