Merge pull request #171 from GetStream/qa-fix-attachments

Qa fix attachments
This commit is contained in:
Salvatore Giordano
2020-12-11 15:38:57 +01:00
committed by GitHub
5 changed files with 272 additions and 63 deletions
+135 -12
View File
@@ -1,38 +1,79 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:video_compress/video_compress.dart';
import 'package:video_player/video_player.dart';
import 'media_utils.dart';
class FileAttachment extends StatelessWidget { enum FileAttachmentType { local, online }
class FileAttachment extends StatefulWidget {
final Attachment attachment; final Attachment attachment;
final Size size; final Size size;
final Widget trailing; final Widget trailing;
final FileAttachmentType attachmentType;
final PlatformFile file;
const FileAttachment({ const FileAttachment({
Key key, Key key,
@required this.attachment, @required this.attachment,
this.size, this.size,
this.trailing, this.trailing,
this.attachmentType = FileAttachmentType.online,
this.file,
}) : super(key: key); }) : super(key: key);
@override
_FileAttachmentState createState() => _FileAttachmentState();
}
class _FileAttachmentState extends State<FileAttachment> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
@override
void initState() {
super.initState();
if (MediaUtils.getMimeType(widget.attachment.title).type == 'video') {
if (widget.attachmentType == FileAttachmentType.online) {
_controller = VideoPlayerController.network(
widget.attachment.assetUrl,
);
} else {
_controller = VideoPlayerController.file(
File.fromRawPath(widget.file.bytes),
);
}
_initializeVideoPlayerFuture = _controller.initialize();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
child: Container( child: Container(
width: size?.width ?? 100, width: widget.size?.width ?? 100,
height: 56.0, height: 56.0,
margin: trailing != null ? EdgeInsets.only(top: 4.0) : null, margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: trailing != null ? BorderRadius.circular(16.0) : null, borderRadius:
border: trailing != null widget.trailing != null ? BorderRadius.circular(16.0) : null,
border: widget.trailing != null
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6))) ? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
: null, : null,
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
child: _getFileTypeImage(attachment.extraData['mime_type']), child: _getFileTypeImage(),
height: 40.0, height: 40.0,
width: 33.33, width: 33.33,
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(8.0),
@@ -46,7 +87,7 @@ class FileAttachment extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
attachment?.title ?? 'File', widget.attachment?.title ?? 'File',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14.0, fontSize: 14.0,
@@ -58,7 +99,7 @@ class FileAttachment extends StatelessWidget {
height: 3.0, height: 3.0,
), ),
Text( Text(
'${attachment.extraData['file_size'] ?? 'N/A'} bytes', '${_getSizeText(widget.attachment.extraData['file_size'])}',
style: TextStyle( style: TextStyle(
color: Colors.black.withOpacity(0.5), color: Colors.black.withOpacity(0.5),
fontSize: 14.0, fontSize: 14.0,
@@ -69,13 +110,13 @@ class FileAttachment extends StatelessWidget {
), ),
Column( Column(
children: [ children: [
trailing ?? widget.trailing ??
IconButton( IconButton(
icon: StreamSvgIcon.cloud_download( icon: StreamSvgIcon.cloud_download(
color: Colors.black, color: Colors.black,
), ),
onPressed: () { onPressed: () {
launchURL(context, attachment.assetUrl); launchURL(context, widget.attachment.assetUrl);
}, },
), ),
], ],
@@ -117,8 +158,76 @@ class FileAttachment extends StatelessWidget {
); );
} }
StreamSvgIcon _getFileTypeImage(String type) { Widget _getFileTypeImage() {
switch (type) { if ((MediaUtils.getMimeType(widget.attachment.title).type == 'image')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return Image.memory(
widget.file.bytes,
fit: BoxFit.cover,
);
break;
case FileAttachmentType.online:
return CachedNetworkImage(
imageUrl: widget.attachment.imageUrl ??
widget.attachment.assetUrl ??
widget.attachment.thumbUrl,
fit: BoxFit.cover,
progressIndicatorBuilder: (context, _, progress) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
backgroundColor: StreamChatTheme.of(context).accentColor,
),
),
);
},
);
break;
}
}
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'video')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return FutureBuilder<File>(
future: VideoCompress.getFileThumbnail(widget.file.path),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
}
return Image.file(
snapshot.data,
fit: BoxFit.cover,
);
},
);
break;
case FileAttachmentType.online:
return FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
);
break;
}
}
switch (widget.attachment.extraData['mime_type']) {
case '7z': case '7z':
return StreamSvgIcon.filetype_7z(); return StreamSvgIcon.filetype_7z();
break; break;
@@ -175,4 +284,18 @@ class FileAttachment extends StatelessWidget {
break; break;
} }
} }
String _getSizeText(int bytes) {
if (bytes == null) {
return 'Size N/A';
}
if (bytes <= 1000) {
return '${bytes} bytes';
} else if (bytes <= 100000) {
return '${(bytes / 1000).toStringAsFixed(2)} KB';
} else {
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
}
}
} }
+1 -1
View File
@@ -180,7 +180,7 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
MediaThumbnailProvider key, DecoderCallback decode) async { MediaThumbnailProvider key, DecoderCallback decode) async {
assert(key == this); assert(key == this);
final bytes = await media.thumbData; final bytes = await media.thumbData;
if (bytes.isEmpty) return null; if (bytes?.isNotEmpty != true) return null;
return await decode(bytes); return await decode(bytes);
} }
+17
View File
@@ -0,0 +1,17 @@
import 'package:http_parser/http_parser.dart' as httpParser;
import 'package:mime/mime.dart';
class MediaUtils {
static httpParser.MediaType getMimeType(String filename) {
httpParser.MediaType mimeType;
if (filename != null) {
if (filename.toLowerCase().endsWith('heic')) {
mimeType = httpParser.MediaType.parse('image/heic');
} else {
mimeType = httpParser.MediaType.parse(lookupMimeType(filename));
}
}
return mimeType;
}
}
+117 -49
View File
@@ -326,14 +326,17 @@ class MessageInputState extends State<MessageInput> {
return AnimatedCrossFade( return AnimatedCrossFade(
crossFadeState: crossFadeState:
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: IconButton( firstChild: InkWell(
onPressed: () { onTap: () {
setState(() { setState(() {
_actionsShrunk = false; _actionsShrunk = false;
}); });
}, },
icon: StreamSvgIcon.emptyCircleLeft( child: Padding(
color: StreamChatTheme.of(context).accentColor, padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context).accentColor,
),
), ),
), ),
secondChild: Row( secondChild: Row(
@@ -354,11 +357,12 @@ class MessageInputState extends State<MessageInput> {
child: Container( child: Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0), borderRadius: BorderRadius.circular(24.0),
border: Border.all( border: Border.all(
color: Colors.grey, color: Colors.grey,
), ),
), ),
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -404,26 +408,45 @@ class MessageInputState extends State<MessageInput> {
child: Chip( child: Chip(
backgroundColor: backgroundColor:
StreamChatTheme.of(context).accentColor, StreamChatTheme.of(context).accentColor,
label: Text( padding: EdgeInsets.zero,
_chosenCommand?.name ?? "", labelPadding:
style: TextStyle(color: Colors.white), EdgeInsets.symmetric(horizontal: 9.0),
), label: Row(
avatar: StreamSvgIcon.lightning( mainAxisSize: MainAxisSize.min,
color: Colors.white, mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.lightning(
color: Colors.white,
size: 16.0,
),
Text(
_chosenCommand?.name?.toUpperCase() ?? "",
style: TextStyle(
color: Colors.white, fontSize: 12.0),
),
],
), ),
), ),
) )
: null, : null,
suffixIcon: _commandEnabled suffixIcon: _commandEnabled
? IconButton( ? InkWell(
icon: Icon(Icons.cancel_outlined), child: Padding(
onPressed: () { padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: StreamSvgIcon.close_small(),
),
onTap: () {
setState(() { setState(() {
_commandEnabled = false; _commandEnabled = false;
}); });
}, },
) )
: null, : null,
suffixIconConstraints: BoxConstraints(
maxHeight: 24.0,
maxWidth: 40.0,
),
), ),
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
), ),
@@ -664,14 +687,18 @@ class MessageInputState extends State<MessageInput> {
Color _getIconColor(int index) { Color _getIconColor(int index) {
switch (index) { switch (index) {
case 0: case 0:
return _attachmentContainsFile && _attachments.isNotEmpty return _attachments.isEmpty
? Colors.black.withOpacity(0.2) ? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5); : (!_attachmentContainsFile
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.2));
break; break;
case 1: case 1:
return !_attachmentContainsFile && _attachments.isNotEmpty return _attachmentContainsFile
? Colors.black.withOpacity(0.2) ? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5); : (_attachments.isEmpty
? Colors.black.withOpacity(0.5)
: Colors.black.withOpacity(0.2));
break; break;
case 2: case 2:
return _attachmentContainsFile && _attachments.isNotEmpty return _attachmentContainsFile && _attachments.isNotEmpty
@@ -799,7 +826,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildPickerSection() { Widget _buildPickerSection() {
var _attachmentContainsFile = var _attachmentContainsFile =
_attachments.any((element) => element.attachment.type == 'file'); _attachments.any((element) => element.attachment?.type == 'file');
switch (_filePickerIndex) { switch (_filePickerIndex) {
case 0: case 0:
@@ -813,22 +840,38 @@ class MessageInputState extends State<MessageInput> {
} }
if (snapshot.data) { if (snapshot.data) {
return IgnorePointer( if (_attachmentContainsFile) {
ignoring: _attachmentContainsFile, return GestureDetector(
child: MediaListView( onTap: () {
selectedIds: _attachments.map((e) => e.id).toList(), pickFile(DefaultAttachmentTypes.file);
onSelect: (media) async {
if (!_attachments
.any((element) => element.id == media.id)) {
_addAttachment(media);
} else {
setState(() {
_attachments
.removeWhere((element) => element.id == media.id);
});
}
}, },
), child: Container(
constraints: BoxConstraints.expand(),
color: Color(0xfff2f2f2),
child: Text(
'Add more files',
style: TextStyle(
color: StreamChatTheme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.center,
),
);
}
return MediaListView(
selectedIds: _attachments.map((e) => e.id).toList(),
onSelect: (media) async {
if (!_attachments
.any((element) => element.id == media.id)) {
_addAttachment(media);
} else {
setState(() {
_attachments
.removeWhere((element) => element.id == media.id);
});
}
},
); );
} }
@@ -1253,6 +1296,8 @@ class MessageInputState extends State<MessageInput> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: FileAttachment( child: FileAttachment(
attachment: e.attachment, attachment: e.attachment,
attachmentType: FileAttachmentType.local,
file: e.file,
size: Size( size: Size(
MediaQuery.of(context).size.width * 0.55, MediaQuery.of(context).size.width * 0.55,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
@@ -1442,16 +1487,31 @@ class MessageInputState extends State<MessageInput> {
padding: padding:
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0), const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
child: StreamSvgIcon.lightning( child: StreamSvgIcon.lightning(
color: Color(0xFF000000).withAlpha(128), color: _commandsOverlay != null
? StreamChatTheme.of(context).accentColor
: Color(0xFF000000).withAlpha(128),
), ),
), ),
onTap: () { onTap: () async {
if (_openFilePickerSection) {
setState(() {
_animateContainer = false;
_openFilePickerSection = false;
_filePickerSize = _kMinMediaPickerSize;
});
await Future.delayed(Duration(milliseconds: 300));
}
if (_commandsOverlay == null) { if (_commandsOverlay == null) {
_commandsOverlay = _buildCommandsOverlayEntry(); setState(() {
Overlay.of(context).insert(_commandsOverlay); _commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
});
} else { } else {
_commandsOverlay?.remove(); setState(() {
_commandsOverlay = null; _commandsOverlay?.remove();
_commandsOverlay = null;
});
} }
}, },
); );
@@ -1646,12 +1706,16 @@ class MessageInputState extends State<MessageInput> {
final mimeType = _getMimeType(file.path.split('/').last); final mimeType = _getMimeType(file.path.split('/').last);
if (mimeType.type == 'video' || mimeType.type == 'image') {
attachmentType = mimeType.type;
}
Map<String, dynamic> extraDataMap = {}; Map<String, dynamic> extraDataMap = {};
if (camera) {
if (mimeType.type == 'video' || mimeType.type == 'image') {
attachmentType = mimeType.type;
}
} else {
attachmentType = 'file';
}
if (mimeType?.subtype != null) { if (mimeType?.subtype != null) {
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase(); extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
} }
@@ -1667,7 +1731,7 @@ class MessageInputState extends State<MessageInput> {
localUri: file.path != null ? Uri.parse(file.path) : null, localUri: file.path != null ? Uri.parse(file.path) : null,
type: attachmentType, type: attachmentType,
extraData: extraDataMap.isNotEmpty ? extraDataMap : null, extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
title: file.name ?? 'File', title: file.name,
), ),
); );
@@ -1784,7 +1848,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildIdleSendButton(BuildContext context) { Widget _buildIdleSendButton(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: Center( child: Center(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
@@ -1793,6 +1857,8 @@ class MessageInputState extends State<MessageInput> {
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: _getIdleSendIcon(), assetName: _getIdleSendIcon(),
color: Colors.grey, color: Colors.grey,
height: 24.0,
width: 24.0,
), ),
)), )),
); );
@@ -1801,7 +1867,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildSendButton(BuildContext context) { Widget _buildSendButton(BuildContext context) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
sendMessage(); sendMessage();
@@ -1809,6 +1875,8 @@ class MessageInputState extends State<MessageInput> {
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: _getSendIcon(), assetName: _getSendIcon(),
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context).accentColor,
height: 24.0,
width: 24.0,
), ),
), ),
), ),
+2 -1
View File
@@ -78,8 +78,9 @@ class UrlAttachment extends StatelessWidget {
children: [ children: [
if (urlAttachment.title != null) if (urlAttachment.title != null)
Text( Text(
urlAttachment.title, urlAttachment.title.trim(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 12.0, fontSize: 12.0,