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/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 56c9dbf0..c84ef58e 100644 --- a/example/lib/advanced_options_page.dart +++ b/example/lib/advanced_options_page.dart @@ -282,6 +282,10 @@ class _AdvancedOptionsPageState extends State { }), userToken, ); + + if (!kIsWeb) { + initNotifications(client); + } } catch (e) { var errorText = 'Error connecting, retry'; if (e is Map) { diff --git a/example/lib/main.dart b/example/lib/main.dart index d0b39d98..500b6059 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -30,6 +30,9 @@ void main() async { User(id: userId), token, ); + if (!kIsWeb) { + initNotifications(client); + } } runApp(MyApp(client)); diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 225d327b..da21111b 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -44,6 +44,7 @@ class _MediaListViewState extends State { placeholder: MemoryImage(kTransparentImage), image: MediaThumbnailProvider( media: media, + highQuality: true, ), fit: BoxFit.cover, ), diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6024169b..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,27 +366,6 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - onChanged: (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, context); - - _checkMentions(s, context); - - _checkEmoji(s, context); - }, style: Theme.of(context).textTheme.bodyText2, autofocus: false, textAlignVertical: TextAlignVertical.center, @@ -440,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'; @@ -451,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(':'); @@ -478,13 +482,13 @@ class MessageInputState extends State { } void _checkMentions(String s, BuildContext context) { - if (textEditingController.selection.isCollapsed && - (s.isNotEmpty && 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); } @@ -654,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), @@ -684,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, ); }); @@ -753,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); + }); } }, ); @@ -813,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, ); @@ -849,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 @@ -868,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, ); } @@ -1221,6 +1210,10 @@ class MessageInputState extends State { ); } + if (attachment.attachment == null) { + return SizedBox(); + } + switch (attachment.attachment.type) { case 'image': case 'giphy': @@ -1230,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, @@ -1314,7 +1305,7 @@ class MessageInputState extends State { setState(() { _animateContainer = true; _openFilePickerSection = false; - _filePickerSize = 250.0; + _filePickerSize = _kMinMediaPickerSize; }); } else { final status = await (Platform.isAndroid @@ -1450,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, @@ -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 804c9728..4a4bfa2e 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 @@ -36,7 +36,8 @@ dependencies: flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 - media_gallery: ^0.1.5 + media_gallery: + git: https://github.com/imtoori/media_gallery.git permission_handler: ^5.0.1+1 transparent_image: ^1.0.0 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 @@ + + +