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(
|
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
@@ -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
|
||||||
/// 
|
/// 
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user