add AttachmentThumbnailBuilder and expose messageinputstate
This commit is contained in:
+143
-105
@@ -17,6 +17,8 @@ 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 {
|
enum ActionsLocation {
|
||||||
LEFT,
|
LEFT,
|
||||||
@@ -70,6 +72,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,
|
||||||
@@ -81,6 +84,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.textEditingController,
|
this.textEditingController,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.actionsLocation = ActionsLocation.LEFT,
|
this.actionsLocation = ActionsLocation.LEFT,
|
||||||
|
this.attachmentThumbnailBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -92,6 +96,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;
|
||||||
|
|
||||||
@@ -119,14 +127,31 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// The location of the custom actions
|
/// The location of the custom actions
|
||||||
final ActionsLocation actionsLocation;
|
final ActionsLocation actionsLocation;
|
||||||
|
|
||||||
|
/// Map that defines a builder for an attachment type
|
||||||
|
final Map<String, AttachmentThumbnailBuilder> attachmentThumbnailBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageInputState createState() => _MessageInputState(
|
MessageInputState createState() => MessageInputState(
|
||||||
doFileUploadRequest: doFileUploadRequest,
|
doFileUploadRequest: doFileUploadRequest,
|
||||||
doImageUploadRequest: doImageUploadRequest,
|
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 = [];
|
||||||
@@ -139,7 +164,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
bool _typingStarted = false;
|
bool _typingStarted = false;
|
||||||
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
||||||
|
|
||||||
_MessageInputState({
|
MessageInputState({
|
||||||
this.doImageUploadRequest,
|
this.doImageUploadRequest,
|
||||||
this.doFileUploadRequest,
|
this.doFileUploadRequest,
|
||||||
}) {
|
}) {
|
||||||
@@ -215,7 +240,7 @@ 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: textEditingController,
|
controller: textEditingController,
|
||||||
@@ -473,34 +498,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(
|
||||||
@@ -520,20 +518,59 @@ 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.attachmentThumbnailBuilder
|
||||||
case FileType.image:
|
?.containsKey(attachment.attachment.type) ==
|
||||||
|
true) {
|
||||||
|
return widget.attachmentThumbnailBuilder[attachment.attachment.type](
|
||||||
|
context,
|
||||||
|
attachment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switch (attachment.attachment.type) {
|
||||||
|
case 'image':
|
||||||
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,
|
||||||
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,
|
||||||
@@ -596,7 +633,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('image', false);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -604,7 +641,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('video', false);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -612,7 +649,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('image', true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -620,7 +657,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('video', true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -628,7 +665,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('file', false);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -637,12 +674,34 @@ 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
|
||||||
|
/// The [attachmentType] should be one of 'image', 'video' or 'file'
|
||||||
|
/// If [camera] is true then the camera will open
|
||||||
|
void pickFile(String attachmentType, [bool camera = false]) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_inputEnabled = false;
|
_inputEnabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
File file;
|
File file;
|
||||||
|
FileType type;
|
||||||
|
if (attachmentType == 'image') {
|
||||||
|
type = FileType.image;
|
||||||
|
} else if (attachmentType == 'video') {
|
||||||
|
type = FileType.video;
|
||||||
|
} else if (attachmentType == 'file') {
|
||||||
|
type = FileType.any;
|
||||||
|
}
|
||||||
|
|
||||||
if (camera) {
|
if (camera) {
|
||||||
if (type == FileType.image) {
|
if (type == FileType.image) {
|
||||||
@@ -666,7 +725,10 @@ 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(() {
|
||||||
@@ -675,7 +737,15 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
final url = await _uploadAttachment(file, type, channel);
|
final url = await _uploadAttachment(file, type, channel);
|
||||||
|
|
||||||
attachment.url = url;
|
if (attachmentType == 'image') {
|
||||||
|
attachment.attachment = attachment.attachment.copyWith(
|
||||||
|
imageUrl: url,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
attachment.attachment = attachment.attachment.copyWith(
|
||||||
|
assetUrl: url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
attachment.uploaded = true;
|
attachment.uploaded = true;
|
||||||
@@ -735,7 +805,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,
|
||||||
@@ -746,7 +816,8 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendMessage(BuildContext context) {
|
/// Sends the current message
|
||||||
|
void sendMessage() async {
|
||||||
final text = textEditingController.text.trim();
|
final text = textEditingController.text.trim();
|
||||||
if (text.isEmpty && _attachments.isEmpty) {
|
if (text.isEmpty && _attachments.isEmpty) {
|
||||||
return;
|
return;
|
||||||
@@ -782,15 +853,6 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
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,
|
||||||
@@ -799,10 +861,23 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -811,23 +886,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,10 +929,9 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
textEditingController =
|
textEditingController =
|
||||||
widget.textEditingController ?? TextEditingController();
|
widget.textEditingController ?? TextEditingController();
|
||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null || widget.initialMessage != null) {
|
||||||
_parseExistingMessage(widget.editMessage);
|
_parseExistingMessage(
|
||||||
} else if (widget.initialMessage != null) {
|
widget.editMessage ?? widget.initialMessage != null);
|
||||||
_parseExistingMessage(widget.initialMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,28 +942,10 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
_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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,15 +969,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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user