diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8e87c58..4b3e086a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Flutter action - uses: subosito/flutter-action@v1.3.2 + uses: subosito/flutter-action@v1.4.0 with: channel: 'stable' - name: Get dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 945199b9..7d5b93f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.13+1 + +- Use TextEditingController.addListener instead of TextField.onChanged + ## 0.2.13 - Update llc dependency diff --git a/README.md b/README.md index 7ca34145..ba5de4c1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin. Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements. +### Troubleshooting + +It may happen that you have some problems building the app. +If it seems related to the [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) make sure to check [this page](https://github.com/miguelpruivo/flutter_file_picker/wiki/Troubleshooting) + ## Docs ### Business logic components diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 8aba7787..20c7c514 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -bb5f9103d9045cd6244bcae1f5f343e5 \ No newline at end of file +c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6f89db84..cd96119a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -19,6 +19,7 @@ import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/video_thumbnail.dart'; import 'package:substring_highlight/substring_highlight.dart'; import '../stream_chat_flutter.dart'; @@ -41,6 +42,8 @@ enum DefaultAttachmentTypes { file, } +const _kMinMediaPickerSize = 360.0; + /// Inactive state /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) @@ -182,7 +185,7 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; - double _filePickerSize = 250.0; + double _filePickerSize = _kMinMediaPickerSize; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -363,30 +366,6 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - onChanged: (s) { - StreamChannel.of(context) - .channel - .keyStroke() - .catchError((e) {}); - - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - _actionsShrunk = s.trim().isNotEmpty; - }); - - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - _emojiOverlay?.remove(); - _emojiOverlay = null; - - _checkCommands(s, context); - - _checkMentions(s, context); - - _checkEmoji(s, context); - }, style: Theme.of(context).textTheme.bodyText2, autofocus: false, textAlignVertical: TextAlignVertical.center, @@ -443,6 +422,28 @@ class MessageInputState extends State { ); } + void _onChanged(BuildContext context, String s) { + StreamChannel.of(context).channel.keyStroke(); + + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + _actionsShrunk = s.trim().isNotEmpty; + }); + + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + _emojiOverlay?.remove(); + _emojiOverlay = null; + + _checkCommands(s.trim(), context); + + _checkMentions(s, context); + + _checkEmoji(s, context); + } + String _getHint() { if (_commandEnabled && _chosenCommand.name == 'giphy') { return 'Search GIFs'; @@ -454,14 +455,14 @@ class MessageInputState extends State { } void _checkEmoji(String s, BuildContext context) { - if (textEditingController.selection.isCollapsed && - (s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' || - textEditingController.text - .substring( - 0, - textEditingController.selection.start, - ) - .contains(':'))) { + if (s.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring( + 0, + textEditingController.selection.baseOffset, + ) + .contains(':')) { final textToSelection = textEditingController.text .substring(0, textEditingController.value.selection.start); final splits = textToSelection.split(':'); @@ -481,15 +482,13 @@ class MessageInputState extends State { } void _checkMentions(String s, BuildContext context) { - if (textEditingController.selection.isCollapsed && - (s.isNotEmpty && - textEditingController.selection.start > 0 && - s[textEditingController.selection.start - 1] == '@' || - textEditingController.text - .substring(0, textEditingController.selection.start) - .split(' ') - .last - .contains('@'))) { + if (s.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) + .split(' ') + .last + .contains('@')) { _mentionsOverlay = _buildMentionsOverlayEntry(); Overlay.of(context).insert(_mentionsOverlay); } @@ -659,9 +658,11 @@ class MessageInputState extends State { }, ), IconButton( - icon: Icon( - StreamIcons.camera, - size: 24, + icon: SvgPicture.asset( + 'svgs/icon_camera.svg', + package: 'stream_chat_flutter', + height: 24, + width: 24, color: _filePickerIndex == 2 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -689,7 +690,7 @@ class MessageInputState extends State { setState(() { _animateContainer = false; _filePickerSize = (_filePickerSize - update.delta.dy).clamp( - 240.0, + _kMinMediaPickerSize, MediaQuery.of(context).size.height / 1.7, ); }); @@ -758,9 +759,10 @@ class MessageInputState extends State { .any((element) => element.id == media.id)) { _addAttachment(media); } else { - _attachments - .removeWhere((element) => element.id == media.id); - setState(() {}); + setState(() { + _attachments + .removeWhere((element) => element.id == media.id); + }); } }, ); @@ -818,35 +820,7 @@ class MessageInputState extends State { } void _addAttachment(Media medium) async { - final mediaFile = await medium.getFile(); - final thumbBytes = await medium.getThumbnail(); - - final file = PlatformFile( - path: mediaFile.path, - bytes: mediaFile.readAsBytesSync(), - ); - - final thumbFile = PlatformFile( - bytes: thumbBytes, - name: '${file.name ?? file.path?.split('/')?.last}_thumbnail.jpeg', - ); - - setState(() { - _inputEnabled = true; - }); - - if (file == null) { - return; - } - - final channel = StreamChannel.of(context).channel; final attachment = _SendingAttachment( - file: file, - thumbFile: thumbFile, - attachment: Attachment( - localUri: file.path != null ? Uri.parse(file.path) : null, - type: medium.mediaType == MediaType.image ? 'image' : 'video', - ), id: medium.id, ); @@ -854,11 +828,23 @@ class MessageInputState extends State { _attachments.add(attachment); }); - final thumbUrl = await _uploadImage( - thumbFile, - channel, + final mediaFile = await medium.getFile(); + + final file = PlatformFile( + path: mediaFile.path, + bytes: mediaFile.readAsBytesSync(), ); + final channel = StreamChannel.of(context).channel; + setState(() { + attachment + ..file = file + ..attachment = Attachment( + localUri: file.path != null ? Uri.parse(file.path) : null, + type: medium.mediaType == MediaType.image ? 'image' : 'video', + ); + }); + final url = await _uploadAttachment( file, medium.mediaType == MediaType.image @@ -873,12 +859,10 @@ class MessageInputState extends State { if (fileType == DefaultAttachmentTypes.image) { attachment.attachment = attachment.attachment.copyWith( imageUrl: url, - thumbUrl: thumbUrl, ); } else { attachment.attachment = attachment.attachment.copyWith( assetUrl: url, - thumbUrl: thumbUrl, ); } @@ -1226,6 +1210,10 @@ class MessageInputState extends State { ); } + if (attachment.attachment == null) { + return SizedBox(); + } + switch (attachment.attachment.type) { case 'image': case 'giphy': @@ -1235,22 +1223,20 @@ class MessageInputState extends State { fit: BoxFit.cover, ) : Image.network( - attachment.attachment.imageUrl ?? - attachment.attachment.thumbUrl, + attachment.attachment.imageUrl, fit: BoxFit.cover, ); break; case 'video': return Stack( children: [ - Container( - child: attachment.thumbFile != null - ? Image.memory( - attachment.thumbFile.bytes, - fit: BoxFit.cover, - ) - : Icon(Icons.videocam), - color: Colors.black26, + Positioned.fill( + child: Container( + child: VideoThumbnail( + file: File( + attachment.file.path, + )), + ), ), Positioned( left: 8, @@ -1319,7 +1305,7 @@ class MessageInputState extends State { setState(() { _animateContainer = true; _openFilePickerSection = false; - _filePickerSize = 250.0; + _filePickerSize = _kMinMediaPickerSize; }); } else { final status = await (Platform.isAndroid @@ -1455,6 +1441,9 @@ class MessageInputState extends State { } else if (fileType == DefaultAttachmentTypes.video) { pickedFile = await _imagePicker.getVideo(source: ImageSource.camera); } + if (pickedFile == null) { + return; + } final bytes = await pickedFile.readAsBytes(); file = PlatformFile( path: pickedFile.path, @@ -1574,9 +1563,14 @@ class MessageInputState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Center( - child: Icon( - _getIdleSendIcon(), - color: Colors.grey, + child: InkWell( + onTap: () { + sendMessage(); + }, + child: Icon( + _getIdleSendIcon(), + color: Colors.grey, + ), )), ), ); @@ -1718,11 +1712,7 @@ class MessageInputState extends State { if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { - if (visible) { - _checkCommands(textEditingController.text, context); - _checkMentions(textEditingController.text, context); - _checkEmoji(textEditingController.text, context); - } + _onChanged(context, textEditingController.text); }); } @@ -1732,6 +1722,10 @@ class MessageInputState extends State { _parseExistingMessage(widget.editMessage ?? widget.initialMessage); } + textEditingController.addListener(() { + _onChanged(context, textEditingController.text); + }); + _focusNode.addListener(() { if (_focusNode.hasFocus) { _openFilePickerSection = false; @@ -1774,14 +1768,12 @@ class MessageInputState extends State { class _SendingAttachment { PlatformFile file; - PlatformFile thumbFile; Attachment attachment; bool uploaded; String id; _SendingAttachment({ this.file, - this.thumbFile, this.attachment, this.uploaded = false, this.id, diff --git a/lib/src/video_thumbnail.dart b/lib/src/video_thumbnail.dart new file mode 100644 index 00000000..c36ab327 --- /dev/null +++ b/lib/src/video_thumbnail.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoThumbnail extends StatefulWidget { + final File file; + + const VideoThumbnail({ + Key key, + @required this.file, + }) : super(key: key); + + @override + _VideoThumbnailState createState() => _VideoThumbnailState(); +} + +class _VideoThumbnailState extends State { + VideoPlayerController _videoPlayerController; + @override + Widget build(BuildContext context) { + return VideoPlayer(_videoPlayerController); + } + + @override + void initState() { + _videoPlayerController = VideoPlayerController.file(widget.file) + ..initialize(); + super.initState(); + } + + @override + void dispose() { + _videoPlayerController.dispose(); + super.dispose(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 7e7fe36b..a39695a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.13 +version: 0.2.13+1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/svgs/icon_camera.svg b/svgs/icon_camera.svg new file mode 100644 index 00000000..0bf3122d --- /dev/null +++ b/svgs/icon_camera.svg @@ -0,0 +1,3 @@ + + +