[LLC, UI-KIT] Add support for custom attachment upload delegate

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2021-02-05 14:57:17 +05:30
parent f18d2c37bf
commit 628542f99b
12 changed files with 418 additions and 243 deletions
+10 -2
View File
@@ -223,19 +223,27 @@ class Channel {
} }
/// Send a file to this channel /// Send a file to this channel
Future<SendFileResponse> sendFile(MultipartFile file) async { Future<SendFileResponse> sendFile(
MultipartFile file, {
ProgressCallback onSendProgress,
}) async {
final response = await _client.post( final response = await _client.post(
'$_channelURL/file', '$_channelURL/file',
data: FormData.fromMap({'file': file}), data: FormData.fromMap({'file': file}),
onSendProgress: onSendProgress,
); );
return _client.decode(response.data, SendFileResponse.fromJson); return _client.decode(response.data, SendFileResponse.fromJson);
} }
/// Send an image to this channel /// Send an image to this channel
Future<SendImageResponse> sendImage(MultipartFile file) async { Future<SendImageResponse> sendImage(
MultipartFile file, {
ProgressCallback onSendProgress,
}) async {
final response = await _client.post( final response = await _client.post(
'$_channelURL/image', '$_channelURL/image',
data: FormData.fromMap({'file': file}), data: FormData.fromMap({'file': file}),
onSendProgress: onSendProgress,
); );
return _client.decode(response.data, SendImageResponse.fromJson); return _client.decode(response.data, SendImageResponse.fromJson);
} }
+6 -1
View File
@@ -794,9 +794,14 @@ class StreamChatClient {
Future<Response<String>> post( Future<Response<String>> post(
String path, { String path, {
dynamic data, dynamic data,
ProgressCallback onSendProgress,
}) async { }) async {
try { try {
final response = await httpClient.post<String>(path, data: data); final response = await httpClient.post<String>(
path,
data: data,
onSendProgress: onSendProgress,
);
return response; return response;
} on DioError catch (error) { } on DioError catch (error) {
throw _parseError(error); throw _parseError(error);
@@ -2,6 +2,7 @@ library stream_chat;
export 'package:dio/src/dio_error.dart'; export 'package:dio/src/dio_error.dart';
export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/multipart_file.dart';
export 'package:dio/src/options.dart' show ProgressCallback;
export 'package:logging/logging.dart' show Logger, Level; export 'package:logging/logging.dart' show Logger, Level;
export './src/api/channel.dart'; export './src/api/channel.dart';
@@ -0,0 +1,57 @@
import 'package:file_picker/file_picker.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'extension.dart';
abstract class AttachmentUploader {
Future<String> uploadImage(
PlatformFile file, {
ProgressCallback onSendProgress,
});
Future<String> uploadFile(
PlatformFile file, {
ProgressCallback onSendProgress,
});
}
class StreamAttachmentUploader implements AttachmentUploader {
final Channel _channel;
const StreamAttachmentUploader(this._channel);
@override
Future<String> uploadImage(
PlatformFile file, {
ProgressCallback onSendProgress,
}) async {
final filename = file.path?.split('/')?.last;
final mimeType = filename.mimeType;
final res = await _channel.sendImage(
await MultipartFile.fromFile(
file.path,
filename: filename,
contentType: mimeType,
),
onSendProgress: onSendProgress,
);
return res.file;
}
@override
Future<String> uploadFile(
PlatformFile file, {
ProgressCallback onSendProgress,
}) async {
final filename = file.path?.split('/')?.last;
final mimeType = filename.mimeType;
final res = await _channel.sendFile(
await MultipartFile.fromFile(
file.path,
filename: filename,
contentType: mimeType,
),
onSendProgress: onSendProgress,
);
return res.file;
}
}
@@ -6,9 +6,10 @@ import 'package:video_compress/video_compress.dart';
class ICompressVideoService { class ICompressVideoService {
static final ICompressVideoService instance = ICompressVideoService._(); static final ICompressVideoService instance = ICompressVideoService._();
final _lock = Lock(); final _lock = Lock();
ICompressVideoService._(); ICompressVideoService._();
Future<MediaInfo> compressVideo(String path) async { Future<MediaInfo> compress(String path) async {
return _lock.synchronized(() { return _lock.synchronized(() {
return VideoCompress.compressVideo( return VideoCompress.compressVideo(
path, path,
@@ -1,5 +1,7 @@
import 'package:characters/characters.dart'; import 'package:characters/characters.dart';
import 'package:emojis/emoji.dart'; import 'package:emojis/emoji.dart';
import 'package:http_parser/http_parser.dart' as http_parser;
import 'package:mime/mime.dart';
final _emojis = Emoji.all(); final _emojis = Emoji.all();
@@ -10,15 +12,27 @@ extension StringExtension on String {
return '${this[0].toUpperCase()}${substring(1)}'; return '${this[0].toUpperCase()}${substring(1)}';
} }
// Emojis guidelines /// Returns whether the string contains only emoji's or not.
// 1 to 3 emojis: big size with no text bubble. ///
// 4+ emojis or emojis+text: standard size with text bubble. /// Emojis guidelines
/// 1 to 3 emojis: big size with no text bubble.
/// 4+ emojis or emojis+text: standard size with text bubble.
bool get isOnlyEmoji { bool get isOnlyEmoji {
final characters = trim().characters; final characters = trim().characters;
if (characters.isEmpty) return false; if (characters.isEmpty) return false;
if (characters.length > 3) return false; if (characters.length > 3) return false;
return characters.every((c) => _emojis.map((e) => e.char).contains(c)); return characters.every((c) => _emojis.map((e) => e.char).contains(c));
} }
/// Returns the mime type from the passed file name.
http_parser.MediaType get mimeType {
if (this == null) return null;
if (toLowerCase().endsWith('heic')) {
return http_parser.MediaType.parse('image/heic');
} else {
return http_parser.MediaType.parse(lookupMimeType(this));
}
}
} }
/// List extension /// List extension
@@ -92,7 +92,7 @@ class _FileAttachmentState extends State<FileAttachment> {
), ),
SizedBox(height: 3.0), SizedBox(height: 3.0),
Text( Text(
'${getSizeText(widget.attachment.extraData['file_size'])}', '${filesize(widget.attachment.extraData['file_size'])}',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.textTheme .textTheme
.footnote .footnote
@@ -9,9 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:http_parser/http_parser.dart' as http_parser;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/compress_video_service.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/media_list_view.dart';
@@ -24,10 +22,10 @@ import 'package:substring_highlight/substring_highlight.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'attachment_uploader.dart';
import 'extension.dart'; import 'extension.dart';
import 'quoted_message_widget.dart'; import 'quoted_message_widget.dart';
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
typedef AttachmentThumbnailBuilder = Widget Function( typedef AttachmentThumbnailBuilder = Widget Function(
BuildContext, BuildContext,
_SendingAttachment, _SendingAttachment,
@@ -101,8 +99,7 @@ class MessageInput extends StatefulWidget {
this.maxHeight = 150, this.maxHeight = 150,
this.keyboardType = TextInputType.multiline, this.keyboardType = TextInputType.multiline,
this.disableAttachments = false, this.disableAttachments = false,
this.doImageUploadRequest, this.attachmentUploader,
this.doFileUploadRequest,
this.initialMessage, this.initialMessage,
this.textEditingController, this.textEditingController,
this.actions, this.actions,
@@ -138,11 +135,8 @@ class MessageInput extends StatefulWidget {
/// If true the attachments button will not be displayed /// If true the attachments button will not be displayed
final bool disableAttachments; final bool disableAttachments;
/// Override image upload request /// A delegate to upload attachments
final FileUploader doImageUploadRequest; final AttachmentUploader attachmentUploader;
/// Override file upload request
final FileUploader doFileUploadRequest;
/// The text controller of the TextField /// The text controller of the TextField
final TextEditingController textEditingController; final TextEditingController textEditingController;
@@ -184,7 +178,7 @@ class MessageInput extends StatefulWidget {
} }
class MessageInputState extends State<MessageInput> { class MessageInputState extends State<MessageInput> {
final List<_SendingAttachment> _attachments = []; final _attachments = <String, _SendingAttachment>{};
final List<User> _mentionedUsers = []; final List<User> _mentionedUsers = [];
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
@@ -210,6 +204,40 @@ class MessageInputState extends State<MessageInput> {
bool get _hasQuotedMessage => widget.quotedMessage != null; bool get _hasQuotedMessage => widget.quotedMessage != null;
AttachmentUploader _attachmentUploader;
@override
void initState() {
super.initState();
_focusNode = widget.focusNode ?? FocusNode();
_emojiNames = Emoji.all().map((e) => e.name);
if (!kIsWeb) {
_keyboardListener =
_keyboardVisibilityController.onChange.listen((visible) {
if (_focusNode.hasFocus) {
_onChanged(context, textEditingController.text);
}
});
}
textEditingController =
widget.textEditingController ?? TextEditingController();
if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
}
textEditingController.addListener(() {
_onChanged(context, textEditingController.text);
});
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_openFilePickerSection = false;
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = SafeArea( Widget child = SafeArea(
@@ -364,7 +392,7 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: AnimatedCrossFade( child: AnimatedCrossFade(
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
_attachments.every((a) => a.uploaded == true)) _attachments.values.every((a) => a.isUploaded == true))
? CrossFadeState.showFirst ? CrossFadeState.showFirst
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: _buildSendButton(context), firstChild: _buildSendButton(context),
@@ -757,8 +785,9 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildFilePickerSection() { Widget _buildFilePickerSection() {
var _attachmentContainsFile = final _attachmentContainsFile = _attachments.values.any((it) {
_attachments.any((element) => element?.attachment?.type == 'file'); return it.attachmentType == 'file';
});
Color _getIconColor(int index) { Color _getIconColor(int index) {
switch (index) { switch (index) {
@@ -913,8 +942,9 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildPickerSection() { Widget _buildPickerSection() {
var _attachmentContainsFile = final _attachmentContainsFile = _attachments.values.any((it) {
_attachments.any((element) => element.attachment?.type == 'file'); return it.attachmentType == 'file';
});
switch (_filePickerIndex) { switch (_filePickerIndex) {
case 0: case 0:
@@ -949,16 +979,12 @@ class MessageInputState extends State<MessageInput> {
); );
} }
return MediaListView( return MediaListView(
selectedIds: _attachments.map((e) => e.id).toList(), selectedIds: _attachments.keys.toList(),
onSelect: (media) async { onSelect: (media) async {
if (!_attachments if (!_attachments.containsKey(media.id)) {
.any((element) => element.id == media.id)) {
_addAttachment(media); _addAttachment(media);
} else { } else {
setState(() { setState(() => _attachments.remove(media.id));
_attachments
.removeWhere((element) => element.id == media.id);
});
} }
}, },
); );
@@ -1019,14 +1045,9 @@ class MessageInputState extends State<MessageInput> {
} }
void _addAttachment(AssetEntity medium) async { void _addAttachment(AssetEntity medium) async {
final attachment = _SendingAttachment( final attachmentId = medium.id;
id: medium.id, _attachments[attachmentId] = _SendingAttachment(id: attachmentId);
);
try { try {
setState(() {
_attachments.add(attachment);
});
final mediaFile = await medium.originFile.timeout( final mediaFile = await medium.originFile.timeout(
Duration(seconds: 5), Duration(seconds: 5),
onTimeout: () => medium.originFile, onTimeout: () => medium.originFile,
@@ -1040,15 +1061,13 @@ class MessageInputState extends State<MessageInput> {
if (file.size > _kMaxAttachmentSize) { if (file.size > _kMaxAttachmentSize) {
if (medium?.type == AssetType.video) { if (medium?.type == AssetType.video) {
final mediaInfo = await compressVideoService.compressVideo(file.path); final mediaInfo = await compressVideoService.compress(file.path);
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) { if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
_showErrorAlert( _showErrorAlert(
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', '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(attachmentId);
_attachments.remove(attachment);
});
return; return;
} }
file = PlatformFile( file = PlatformFile(
@@ -1064,54 +1083,59 @@ class MessageInputState extends State<MessageInput> {
} }
} }
final channel = StreamChannel.of(context).channel;
setState(() { setState(() {
attachment _attachments.update(attachmentId, (it) {
..file = file return it.copyWith(
..attachment = Attachment( file: file,
localUri: file.path != null ? Uri.parse(file.path) : null, attachment: Attachment(
type: medium?.type == AssetType.image ? 'image' : 'video', localUri: file.path != null ? Uri.parse(file.path) : null,
type: medium?.type == AssetType.image ? 'image' : 'video',
),
); );
});
}); });
final url = await _uploadAttachment(
file,
medium.type == AssetType.image
? DefaultAttachmentTypes.image
: DefaultAttachmentTypes.video,
channel);
final fileType = medium.type == AssetType.image final fileType = medium.type == AssetType.image
? DefaultAttachmentTypes.image ? DefaultAttachmentTypes.image
: DefaultAttachmentTypes.video; : DefaultAttachmentTypes.video;
final url = await _uploadAttachment(
file,
fileType,
onSendProgress: (sent, total) {
setState(() {
_attachments.update(
attachmentId,
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
);
});
},
);
if (fileType == DefaultAttachmentTypes.image) { if (fileType == DefaultAttachmentTypes.image) {
attachment.attachment = attachment.attachment.copyWith( _attachments.update(attachmentId, (it) {
imageUrl: url, return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
); });
} else { } else {
attachment.attachment = attachment.attachment.copyWith( _attachments.update(attachmentId, (it) {
assetUrl: url, return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
); });
} }
if (mounted) { if (mounted) {
setState(() { setState(() {
attachment.uploaded = true; // Marking as upload complete
_attachments.update(
attachmentId,
(it) => it.copyWith(totalUploaded: it.totalSize),
);
}); });
} }
} catch (e, s) { } catch (e, s) {
setState(() { setState(() => _attachments.remove(attachmentId));
_attachments.remove(attachment);
});
print(e); print(e);
print(s); print(s);
// ignore: deprecated_member_use _showErrorAlert('Error adding the attachment: $e');
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Error adding the attachment: $e'),
),
);
} }
} }
@@ -1536,9 +1560,15 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachments() { Widget _buildAttachments() {
if (_attachments.isEmpty) return Offstage(); if (_attachments.isEmpty) return Offstage();
final fileAttachments = _attachments.values
.where((it) => it.attachmentType == 'file')
.toList(growable: false);
final remainingAttachments = _attachments.values
.where((it) => it.attachmentType != 'file')
.toList(growable: false);
return Column( return Column(
children: [ children: [
if (_attachments.any((e) => e.attachment?.type == 'file')) if (fileAttachments.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: LimitedBox( child: LimitedBox(
@@ -1546,8 +1576,7 @@ class MessageInputState extends State<MessageInput> {
child: ListView( child: ListView(
reverse: true, reverse: true,
shrinkWrap: true, shrinkWrap: true,
children: _attachments.reversed children: fileAttachments.reversed
.where((e) => e.attachment?.type == 'file')
.map<Widget>( .map<Widget>(
(e) => ClipRRect( (e) => ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@@ -1575,8 +1604,9 @@ class MessageInputState extends State<MessageInput> {
.white, .white,
), ),
), ),
onTap: () => onTap: () {
setState(() => _attachments.remove(e)), setState(() => _attachments.remove(e.id));
},
), ),
), ),
), ),
@@ -1586,15 +1616,14 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
if (_attachments.any((e) => e.attachment?.type != 'file')) if (remainingAttachments.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: LimitedBox( child: LimitedBox(
maxHeight: 104.0, maxHeight: 104.0,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: _attachments children: remainingAttachments
.where((e) => e.attachment?.type != 'file')
.map<Widget>( .map<Widget>(
(attachment) => ClipRRect( (attachment) => ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@@ -1610,16 +1639,15 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
_buildRemoveButton(attachment), _buildRemoveButton(attachment),
attachment.uploaded if (!attachment.isUploaded)
? SizedBox() Positioned.fill(
: Positioned.fill( child: Center(
child: Center( child: Padding(
child: Padding( padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0), child: CircularProgressIndicator(),
child: CircularProgressIndicator(),
),
),
), ),
),
),
], ],
), ),
), ),
@@ -1632,6 +1660,38 @@ class MessageInputState extends State<MessageInput> {
); );
} }
Widget _buildUploadProgressIndicator(int uploaded, int total) {
final theme = StreamChatTheme.of(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorTheme.overlayDark.withOpacity(0.6),
),
child: Padding(
padding: const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
child: Row(
children: [
SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Color(0xffb2b2b2)),
),
),
SizedBox(width: 8),
Text(
'${filesize(uploaded)} / ${filesize(total)}',
style: theme.textTheme.footnote.copyWith(
color: theme.colorTheme.white,
),
),
],
),
),
);
}
Positioned _buildRemoveButton(_SendingAttachment attachment) { Positioned _buildRemoveButton(_SendingAttachment attachment) {
return Positioned( return Positioned(
height: 24, height: 24,
@@ -1648,9 +1708,7 @@ class MessageInputState extends State<MessageInput> {
disabledElevation: 0, disabledElevation: 0,
hoverElevation: 0, hoverElevation: 0,
onPressed: () { onPressed: () {
setState(() { setState(() => _attachments.remove(attachment.id));
_attachments.remove(attachment);
});
}, },
fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5), fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5),
child: Center( child: Center(
@@ -1665,9 +1723,9 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachment(_SendingAttachment attachment) { Widget _buildAttachment(_SendingAttachment attachment) {
if (widget.attachmentThumbnailBuilders if (widget.attachmentThumbnailBuilders
?.containsKey(attachment.attachment?.type) == ?.containsKey(attachment.attachmentType) ==
true) { true) {
return widget.attachmentThumbnailBuilders[attachment.attachment?.type]( return widget.attachmentThumbnailBuilders[attachment.attachmentType](
context, context,
attachment, attachment,
); );
@@ -1677,7 +1735,7 @@ class MessageInputState extends State<MessageInput> {
return SizedBox(); return SizedBox();
} }
switch (attachment.attachment?.type) { switch (attachment.attachmentType) {
case 'image': case 'image':
case 'giphy': case 'giphy':
return attachment.file != null return attachment.file != null
@@ -1895,19 +1953,18 @@ class MessageInputState extends State<MessageInput> {
/// Use this to add custom type attachments /// Use this to add custom type attachments
void addAttachment(Attachment attachment) { void addAttachment(Attachment attachment) {
setState(() { setState(() {
_attachments.add(_SendingAttachment( final _attachment = _SendingAttachment(
attachment: attachment, attachment: attachment,
uploaded: true, totalUploaded: attachment.extraData['file_size'],
)); );
_attachments[_attachment.id] = _attachment;
}); });
} }
/// Pick a file from the device /// Pick a file from the device
/// If [camera] is true then the camera will open /// If [camera] is true then the camera will open
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async { void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
setState(() { setState(() => _inputEnabled = false);
_inputEnabled = false;
});
PlatformFile file; PlatformFile file;
String attachmentType; String attachmentType;
@@ -1954,15 +2011,11 @@ class MessageInputState extends State<MessageInput> {
} }
} }
setState(() { setState(() => _inputEnabled = true);
_inputEnabled = true;
});
if (file == null) { if (file == null) return;
return;
}
final mimeType = _getMimeType(file.path.split('/').last); final mimeType = file.path.split('/').last.mimeType;
var extraDataMap = <String, dynamic>{}; var extraDataMap = <String, dynamic>{};
@@ -1982,7 +2035,6 @@ class MessageInputState extends State<MessageInput> {
extraDataMap['file_size'] = file.size; extraDataMap['file_size'] = file.size;
} }
final channel = StreamChannel.of(context).channel;
final attachment = _SendingAttachment( final attachment = _SendingAttachment(
file: file, file: file,
attachment: Attachment( attachment: Attachment(
@@ -1992,14 +2044,13 @@ class MessageInputState extends State<MessageInput> {
title: file.name, title: file.name,
), ),
); );
final attachmentId = attachment.id;
setState(() { setState(() => _attachments[attachmentId] = attachment);
_attachments.add(attachment);
});
if (file.size / 1024 > _kMaxAttachmentSize) { if (file.size / 1024 > _kMaxAttachmentSize) {
if (attachmentType == 'video') { if (attachmentType == 'video') {
final mediaInfo = await compressVideoService.compressVideo(file.path); final mediaInfo = await compressVideoService.compress(file.path);
file = PlatformFile( file = PlatformFile(
name: mediaInfo.title, name: mediaInfo.title,
size: (mediaInfo.filesize / 1024).ceil(), size: (mediaInfo.filesize / 1024).ceil(),
@@ -2007,98 +2058,74 @@ class MessageInputState extends State<MessageInput> {
path: mediaInfo.path, path: mediaInfo.path,
); );
setState(() { setState(() {
attachment.file = file; _attachments.update(attachmentId, (it) => it.copyWith(file: file));
}); });
} else { } else {
// ignore: deprecated_member_use
_showErrorAlert( _showErrorAlert(
'The file is too large to upload. The file size limit is 20MB.', 'The file is too large to upload. The file size limit is 20MB.',
); );
setState(() { setState(() => _attachments.remove(attachmentId));
_attachments.remove(attachment);
});
return; return;
} }
} }
final url = await _uploadAttachment(file, fileType, channel); try {
final url = await _uploadAttachment(
file,
fileType,
onSendProgress: (sent, total) {
setState(() {
_attachments.update(
attachmentId,
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
);
});
},
);
if (fileType == DefaultAttachmentTypes.image) { if (fileType == DefaultAttachmentTypes.image) {
attachment.attachment = attachment.attachment.copyWith( _attachments.update(attachmentId, (it) {
imageUrl: url, return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
); });
} else { } else {
attachment.attachment = attachment.attachment.copyWith( _attachments.update(attachmentId, (it) {
assetUrl: url, return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
); });
}
if (mounted) {
setState(() {
// Marking as upload complete
_attachments.update(
attachmentId,
(it) => it.copyWith(totalUploaded: it.totalSize),
);
});
}
} catch (e, s) {
setState(() => _attachments.remove(attachmentId));
print(e);
print(s);
_showErrorAlert('Error adding the attachment: $e');
} }
setState(() {
attachment.uploaded = true;
});
} }
Future<String> _uploadAttachment( Future<String> _uploadAttachment(
PlatformFile file, PlatformFile file,
DefaultAttachmentTypes type, DefaultAttachmentTypes type, {
Channel channel, ProgressCallback onSendProgress,
) async { }) {
String url;
if (type == DefaultAttachmentTypes.image) { if (type == DefaultAttachmentTypes.image) {
if (widget.doImageUploadRequest != null) { return _attachmentUploader.uploadImage(
url = await widget.doImageUploadRequest(file, channel); file,
} else { onSendProgress: onSendProgress,
url = await _uploadImage(file, channel); );
}
} else { } else {
if (widget.doFileUploadRequest != null) { return _attachmentUploader.uploadFile(
url = await widget.doFileUploadRequest(file, channel); file,
} else { onSendProgress: onSendProgress,
url = await _uploadFile(file, channel); );
}
} }
return url;
}
Future<String> _uploadImage(PlatformFile file, Channel channel) async {
final filename = file.path?.split('/')?.last;
final mimeType = _getMimeType(filename);
final bytes = file.bytes;
final res = await channel.sendImage(
MultipartFile.fromBytes(
bytes,
filename: filename,
contentType: mimeType,
),
);
return res.file;
}
http_parser.MediaType _getMimeType(String filename) {
http_parser.MediaType mimeType;
if (filename != null) {
if (filename.toLowerCase().endsWith('heic')) {
mimeType = http_parser.MediaType.parse('image/heic');
} else {
mimeType = http_parser.MediaType.parse(lookupMimeType(filename));
}
}
return mimeType;
}
Future<String> _uploadFile(PlatformFile file, Channel channel) async {
final filename = file.path?.split('/')?.last;
final mimeType = _getMimeType(filename);
final bytes = file.bytes;
final res = await channel.sendFile(
MultipartFile.fromBytes(
bytes,
filename: filename,
contentType: mimeType,
),
);
return res.file;
} }
Widget _buildIdleSendButton(BuildContext context) { Widget _buildIdleSendButton(BuildContext context) {
@@ -2153,7 +2180,7 @@ class MessageInputState extends State<MessageInput> {
text = '/${_chosenCommand.name} ' + text; text = '/${_chosenCommand.name} ' + text;
} }
final attachments = List<_SendingAttachment>.from(_attachments); final attachments = [..._attachments.values];
textEditingController.clear(); textEditingController.clear();
_attachments.clear(); _attachments.clear();
@@ -2234,39 +2261,6 @@ class MessageInputState extends State<MessageInput> {
StreamSubscription _keyboardListener; StreamSubscription _keyboardListener;
@override
void initState() {
super.initState();
_focusNode = widget.focusNode ?? FocusNode();
_emojiNames = Emoji.all().map((e) => e.name);
if (!kIsWeb) {
_keyboardListener =
_keyboardVisibilityController.onChange.listen((visible) {
if (_focusNode.hasFocus) {
_onChanged(context, textEditingController.text);
}
});
}
textEditingController =
widget.textEditingController ?? TextEditingController();
if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
}
textEditingController.addListener(() {
_onChanged(context, textEditingController.text);
});
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
_openFilePickerSection = false;
}
});
}
void _showErrorAlert(String description) { void _showErrorAlert(String description) {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.white,
@@ -2344,10 +2338,11 @@ class MessageInputState extends State<MessageInput> {
_messageIsPresent = true; _messageIsPresent = true;
message.attachments?.forEach((attachment) { message.attachments?.forEach((attachment) {
_attachments.add(_SendingAttachment( final _attachment = _SendingAttachment(
attachment: attachment, attachment: attachment,
uploaded: true, totalUploaded: attachment.extraData['file_size'],
)); );
_attachments[_attachment.id] = _attachment;
}); });
} }
@@ -2368,22 +2363,61 @@ class MessageInputState extends State<MessageInput> {
FocusScope.of(context).requestFocus(_focusNode); FocusScope.of(context).requestFocus(_focusNode);
_initialized = true; _initialized = true;
} }
final channel = StreamChannel.of(context).channel;
if (_attachmentUploader == null) {
_attachmentUploader =
widget.attachmentUploader ?? StreamAttachmentUploader(channel);
} else if (_attachmentUploader is StreamAttachmentUploader) {
_attachmentUploader = StreamAttachmentUploader(channel);
}
super.didChangeDependencies(); super.didChangeDependencies();
} }
} }
class _SendingAttachment { class _SendingAttachment {
PlatformFile file;
Attachment attachment;
bool uploaded;
String id;
_SendingAttachment({ _SendingAttachment({
String id,
this.file, this.file,
this.attachment, this.attachment,
this.uploaded = false, this.totalUploaded = 0,
this.id, int totalSize,
}); }) : id = id ?? shortHash(DateTime.now().millisecondsSinceEpoch),
attachmentType = attachment?.type,
totalSize =
totalSize ?? file?.size ?? attachment.extraData['file_size'];
final String id;
final PlatformFile file;
final Attachment attachment;
final String attachmentType;
final int totalUploaded;
final int totalSize;
// Progress while the attachment is uploading to the server
// 0 -> 100
double get uploadPercentage {
if (totalSize == null) return null;
return (totalUploaded / totalSize) * 100;
}
bool get isUploaded => uploadPercentage == 100;
_SendingAttachment copyWith({
String id,
PlatformFile file,
Attachment attachment,
int totalUploaded,
int totalSize,
}) {
return _SendingAttachment(
id: id ?? this.id,
file: file ?? this.file,
attachment: attachment ?? this.attachment,
totalUploaded: totalUploaded ?? this.totalUploaded,
totalSize: totalSize ?? this.totalSize,
);
}
} }
/// Represents a 2-tuple, or pair. /// Represents a 2-tuple, or pair.
@@ -214,18 +214,70 @@ String getWebsiteName(String hostName) {
} }
} }
/// /// A method returns a human readable string representing a file _size
String getSizeText(int bytes) { String filesize(dynamic size, [int round = 2]) {
if (bytes == null) { if (size == null) return 'Size N/A';
return 'Size N/A';
/**
* [size] can be passed as number or as string
*
* the optional parameter [round] specifies the number
* of digits after comma/point (default is 2)
*/
final divider = 1024;
int _size;
try {
_size = int.parse(size.toString());
} catch (e) {
throw ArgumentError('Can not parse the size parameter: $e');
} }
if (bytes <= 1000) { if (_size < divider) {
return '$bytes bytes'; return '$_size B';
} else if (bytes <= 100000) { }
return '${(bytes / 1000).toStringAsFixed(2)} KB';
if (_size < divider * divider && _size % divider == 0) {
return '${(_size / divider).toStringAsFixed(0)} KB';
}
if (_size < divider * divider) {
return '${(_size / divider).toStringAsFixed(round)} KB';
}
if (_size < divider * divider * divider && _size % divider == 0) {
return '${(_size / (divider * divider)).toStringAsFixed(0)} MB';
}
if (_size < divider * divider * divider) {
return '${(_size / divider / divider).toStringAsFixed(round)} MB';
}
if (_size < divider * divider * divider * divider && _size % divider == 0) {
return '${(_size / (divider * divider * divider)).toStringAsFixed(0)} GB';
}
if (_size < divider * divider * divider * divider) {
return '${(_size / divider / divider / divider).toStringAsFixed(round)} GB';
}
if (_size < divider * divider * divider * divider * divider &&
_size % divider == 0) {
num r = _size / divider / divider / divider / divider;
return '${r.toStringAsFixed(0)} TB';
}
if (_size < divider * divider * divider * divider * divider) {
num r = _size / divider / divider / divider / divider;
return '${r.toStringAsFixed(round)} TB';
}
if (_size < divider * divider * divider * divider * divider * divider &&
_size % divider == 0) {
num r = _size / divider / divider / divider / divider / divider;
return '${r.toStringAsFixed(0)} PB';
} else { } else {
return '${(bytes / 1000000).toStringAsFixed(2)} MB'; num r = _size / divider / divider / divider / divider / divider;
return '${r.toStringAsFixed(round)} PB';
} }
} }
+2 -1
View File
@@ -11,7 +11,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter_core: ^1.0.1-beta stream_chat_flutter_core:
path: ../stream_chat_flutter_core
flutter_app_badger: ^1.1.2 flutter_app_badger: ^1.1.2
photo_view: ^0.10.3 photo_view: ^0.10.3
rxdart: ^0.25.0 rxdart: ^0.25.0
@@ -10,7 +10,8 @@ environment:
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
stream_chat: ^1.0.2-beta stream_chat:
path: ../stream_chat
flutter: flutter:
sdk: flutter sdk: flutter
rxdart: ^0.25.0 rxdart: ^0.25.0
@@ -13,7 +13,8 @@ dependencies:
path: ^1.7.0 path: ^1.7.0
path_provider: ^1.6.27 path_provider: ^1.6.27
sqlite3_flutter_libs: ^0.3.0 sqlite3_flutter_libs: ^0.3.0
stream_chat: ^1.0.2-beta stream_chat:
path: ../stream_chat
dev_dependencies: dev_dependencies:
test: ^1.15.7 test: ^1.15.7