diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 29b026b2..b028e9b7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -121,9 +121,9 @@ PODS: - Flutter - PromisesObjC (1.2.11) - Protobuf (3.13.0) - - SDWebImage (5.9.4): - - SDWebImage/Core (= 5.9.4) - - SDWebImage/Core (5.9.4) + - SDWebImage (5.10.0): + - SDWebImage/Core (= 5.10.0) + - SDWebImage/Core (5.10.0) - shared_preferences (0.0.1): - Flutter - sqflite (0.0.2): @@ -148,11 +148,13 @@ PODS: - sqlite3/perf-threadsafe - sqlite3/rtree - Starscream (4.0.4) - - StreamChatClient (2.4.1): + - StreamChatClient (2.4.2): - Starscream (~> 4.0) - SwiftyGif (5.3.0) - url_launcher (0.0.1): - Flutter + - video_compress (0.3.0): + - Flutter - video_player (0.0.1): - Flutter - wakelock (0.0.1): @@ -177,6 +179,7 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - StreamChatClient - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player (from `.symlinks/plugins/video_player/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -237,6 +240,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" video_player: :path: ".symlinks/plugins/video_player/ios" wakelock: @@ -270,15 +275,16 @@ SPEC CHECKSUMS: permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 - SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1 + SDWebImage: 9169792e9eec3e45bba2a0c02f74bf8bd922d1ee shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqlite3: 8f7d2078ae27778699a622a94b853285793422a2 sqlite3_flutter_libs: 5651f8ff48e3b44d910863c4ea5916085b1b245f Starscream: 5178aed56b316f13fa3bc55694e583d35dd414d9 - StreamChatClient: d061cf52babcd9df930aee9cf864ab0379427eff + StreamChatClient: fc6419fa7ceda9c048188bb7c8e9f07755ced05f SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e wakelock: bfc7955c418d0db797614075aabbc58a39ab5107 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 709db931..bb101716 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -335,6 +335,7 @@ "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", + "${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework", "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", ); @@ -366,6 +367,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 02c2c726..1ca27846 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.75+77 +version: 1.0.76+78 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index bfd8e477..45c53eb6 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -167,122 +167,123 @@ class _ChannelListViewState extends State ChannelsBlocState channelsBlocState, ) { return StreamBuilder>( - stream: channelsBlocState.channelsStream, - builder: (context, snapshot) { - var child; - if (snapshot.hasError) { - child = _buildErrorWidget( - snapshot, - context, - channelsBlocState, - ); - } else if (!snapshot.hasData) { - child = _buildLoadingWidget(); - } else { - final channels = snapshot.data; + stream: channelsBlocState.channelsStream, + builder: (context, snapshot) { + var child; + if (snapshot.hasError) { + child = _buildErrorWidget( + snapshot, + context, + channelsBlocState, + ); + } else if (!snapshot.hasData) { + child = _buildLoadingWidget(); + } else { + final channels = snapshot.data; - if (channels.isEmpty && widget.emptyBuilder != null) { - child = widget.emptyBuilder(context); - } + if (channels.isEmpty && widget.emptyBuilder != null) { + child = widget.emptyBuilder(context); + } - if (channels.isEmpty && widget.emptyBuilder == null) { - child = LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: StreamSvgIcon.message( - size: 136, - color: Color(0xffDBDBDB), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'Let’s start chatting!', - style: TextStyle( - fontSize: 16, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 52, - ), - child: Text( - 'How about sending your first message to a friend?', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Color(0xff7A7A7A), - ), - ), - ), - ], - ), + if (channels.isEmpty && widget.emptyBuilder == null) { + child = LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, ), - if (widget.onStartChatPressed != null) - Positioned( - right: 0, - left: 0, - bottom: 32, - child: Center( - child: FlatButton( - onPressed: widget.onStartChatPressed, - child: Text( - 'Start a chat', - style: TextStyle( - color: - StreamChatTheme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon.message( + size: 136, + color: Color(0xffDBDBDB), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Let’s start chatting!', + style: TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 52, + ), + child: Text( + 'How about sending your first message to a friend?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Color(0xff7A7A7A), + ), + ), + ), + ], + ), + ), + if (widget.onStartChatPressed != null) + Positioned( + right: 0, + left: 0, + bottom: 32, + child: Center( + child: FlatButton( + onPressed: widget.onStartChatPressed, + child: Text( + 'Start a chat', + style: TextStyle( + color: + StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold, ), ), ), ), - ], - ), - ); - }, - ); - } - - if (channels.isNotEmpty) { - child = ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); - }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), - ); - } + ), + ], + ), + ); + }, + ); } - return AnimatedSwitcher( - child: child, - duration: Duration(milliseconds: 500), - ); - }); + if (channels.isNotEmpty) { + child = ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channels); + }, + childCount: (channels.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channels.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + } + } + + return AnimatedSwitcher( + child: child, + duration: Duration(milliseconds: 500), + ); + }, + ); } Widget _buildLoadingWidget() { diff --git a/lib/src/compress_video_service.dart b/lib/src/compress_video_service.dart new file mode 100644 index 00000000..43d2c79c --- /dev/null +++ b/lib/src/compress_video_service.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'package:synchronized/synchronized.dart'; +import 'package:video_compress/video_compress.dart'; + +class ICompressVideoService { + static final ICompressVideoService instance = ICompressVideoService._(); + final _lock = Lock(); + ICompressVideoService._(); + + Future compressVideo(String path) async { + return _lock.synchronized(() { + return VideoCompress.compressVideo( + path, + ); + }); + } +} + +ICompressVideoService get CompressVideoService => + ICompressVideoService.instance; diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index b4c79033..c9ed15bb 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -14,6 +14,7 @@ import 'package:media_gallery/media_gallery.dart'; import 'package:mime/mime.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/compress_video_service.dart'; 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'; @@ -21,6 +22,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.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 'package:video_compress/video_compress.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -44,6 +46,8 @@ enum DefaultAttachmentTypes { const _kMinMediaPickerSize = 360.0; +const _kMaxAttachmentSize = 20480; //20MB + /// 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) @@ -797,7 +801,6 @@ class MessageInputState extends State { var status = await (Platform.isAndroid ? Permission.storage.status : Permission.photos.status); - print('status: ${status}'); if (status.isPermanentlyDenied || status.isDenied) { if (await openAppSettings()) { setState(() {}); @@ -844,55 +847,102 @@ class MessageInputState extends State { } void _addAttachment(Media medium) async { - final attachment = _SendingAttachment( - id: medium.id, - ); - - setState(() { - _attachments.add(attachment); - }); - - 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 - ? DefaultAttachmentTypes.image - : DefaultAttachmentTypes.video, - channel); - - final fileType = medium.mediaType == MediaType.image - ? DefaultAttachmentTypes.image - : DefaultAttachmentTypes.video; - - if (fileType == DefaultAttachmentTypes.image) { - attachment.attachment = attachment.attachment.copyWith( - imageUrl: url, + try { + final attachment = _SendingAttachment( + id: medium.id, ); - } else { - attachment.attachment = attachment.attachment.copyWith( - assetUrl: url, + + setState(() { + _attachments.add(attachment); + }); + + final mediaFile = await medium.getFile(); + + var file = PlatformFile( + path: mediaFile.path, + size: (await mediaFile.length()) ~/ 1024, + bytes: mediaFile.readAsBytesSync(), + ); + + if (file.size > _kMaxAttachmentSize) { + if (medium.mediaType == MediaType.video) { + final mediaInfo = await CompressVideoService.compressVideo(file.path); + + if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) { + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + 'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', + ), + ), + ); + + setState(() { + _attachments.remove(attachment); + }); + return; + } + file = PlatformFile( + name: file.name, + size: mediaInfo.filesize, + bytes: await mediaInfo.file.readAsBytes(), + path: mediaInfo.path, + ); + } else { + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + 'The file is too large to upload. The file size limit is 20MB', + ), + ), + ); + } + } + + 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 + ? DefaultAttachmentTypes.image + : DefaultAttachmentTypes.video, + channel); + + final fileType = medium.mediaType == MediaType.image + ? DefaultAttachmentTypes.image + : DefaultAttachmentTypes.video; + + if (fileType == DefaultAttachmentTypes.image) { + attachment.attachment = attachment.attachment.copyWith( + imageUrl: url, + ); + } else { + attachment.attachment = attachment.attachment.copyWith( + assetUrl: url, + ); + } + + if (mounted) { + setState(() { + attachment.uploaded = true; + }); + } + } catch (e, s) { + print(s); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text('Error adding the attachment: $e'), + ), ); } - - setState(() { - attachment.uploaded = true; - }); } CircleAvatar _buildGiphyIcon() { @@ -1267,9 +1317,10 @@ class MessageInputState extends State { Positioned.fill( child: Container( child: VideoThumbnail( - file: File( - attachment.file.path, - )), + file: File( + attachment.file.path, + ), + ), ), ), Positioned( @@ -1496,7 +1547,6 @@ class MessageInputState extends State { ); if (res?.files?.isNotEmpty == true) { file = res.files.single; - print('file.bytes?.length: ${file.bytes?.length}'); } } @@ -1508,6 +1558,26 @@ class MessageInputState extends State { return; } + if (file.size > _kMaxAttachmentSize) { + if (attachmentType == 'video') { + final mediaInfo = await CompressVideoService.compressVideo(file.path); + file = PlatformFile( + name: mediaInfo.title, + size: mediaInfo.filesize, + bytes: await mediaInfo.file.readAsBytes(), + path: mediaInfo.path, + ); + } else { + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + 'The file is too large to upload. The file size limit is 20MB', + ), + ), + ); + } + } + final channel = StreamChannel.of(context).channel; final attachment = _SendingAttachment( file: file, diff --git a/pubspec.yaml b/pubspec.yaml index 4ea0f4bb..b0f65c71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: flutter_keyboard_visibility: ^3.3.0 stream_chat: ^0.2.13+1 mime: ^0.9.6+3 + video_compress: ^2.1.1 visibility_detector: ^0.1.5 http_parser: ^3.1.4 lottie: ^0.7.0+1 @@ -42,6 +43,7 @@ dependencies: permission_handler: ^5.0.1+1 transparent_image: ^1.0.0 ezanimation: ^0.4.1 + synchronized: ^2.2.0+2 flutter: assets: