Merge pull request #159 from GetStream/feature/file-attachment-new

Feature/file attachment new
This commit is contained in:
Salvatore Giordano
2020-12-03 12:46:21 +01:00
committed by GitHub
5 changed files with 663 additions and 164 deletions
+156 -10
View File
@@ -1,32 +1,178 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
class FileAttachment extends StatelessWidget {
final Attachment attachment;
final Size size;
final Widget trailing;
const FileAttachment({
Key key,
@required this.attachment,
this.size,
this.trailing,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: () {
launchURL(context, attachment.assetUrl);
},
child: Container(
width: size?.width ?? 100,
height: size?.height ?? 100,
child: Center(
child: Icon(Icons.attach_file),
),
child: Container(
width: size?.width ?? 100,
height: 56.0,
margin: trailing != null ? EdgeInsets.only(top: 4.0) : null,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: trailing != null ? BorderRadius.circular(16.0) : null,
border: trailing != null
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
: null,
),
child: Row(
children: [
Container(
child: _getFileTypeImage(attachment.extraData['mime_type']),
height: 40.0,
width: 33.33,
margin: EdgeInsets.all(8.0),
),
SizedBox(
width: 6.0,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attachment?.title ?? 'File',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 3.0,
),
Text(
'${attachment.extraData['file_size'] ?? 'N/A'} bytes',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 14.0,
),
),
],
),
),
Column(
children: [
trailing ??
IconButton(
icon: StreamSvgIcon.cloud_download(
color: Colors.black,
),
onPressed: () {
launchURL(context, attachment.assetUrl);
},
),
],
),
],
),
// ListTile(
// dense: true,
// leading: Container(
// child: _getFileTypeImage(attachment.extraData['mime_type']),
// height: 40.0,
// width: 33.33,
// ),
// title: Text(
// attachment?.title ?? 'File',
// style: TextStyle(
// fontWeight: FontWeight.bold,
// ),
// maxLines: 3,
// ),
// subtitle: Text(
// '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
// style: TextStyle(
// color: Colors.black.withOpacity(0.5),
// ),
// ),
// trailing: trailing ??
// IconButton(
// icon: StreamSvgIcon.cloud_download(
// color: Colors.black,
// ),
// onPressed: () {
// launchURL(context, attachment.assetUrl);
// },
// ),
// ),
),
);
}
StreamSvgIcon _getFileTypeImage(String type) {
switch (type) {
case '7z':
return StreamSvgIcon.filetype_7z();
break;
case 'csv':
return StreamSvgIcon.filetype_csv();
break;
case 'doc':
return StreamSvgIcon.filetype_doc();
break;
case 'docx':
return StreamSvgIcon.filetype_docx();
break;
case 'html':
return StreamSvgIcon.filetype_html();
break;
case 'md':
return StreamSvgIcon.filetype_md();
break;
case 'odt':
return StreamSvgIcon.filetype_odt();
break;
case 'pdf':
return StreamSvgIcon.filetype_pdf();
break;
case 'ppt':
return StreamSvgIcon.filetype_ppt();
break;
case 'pptx':
return StreamSvgIcon.filetype_pptx();
break;
case 'rar':
return StreamSvgIcon.filetype_rar();
break;
case 'rtf':
return StreamSvgIcon.filetype_rtf();
break;
case 'tar':
return StreamSvgIcon.filetype_tar();
break;
case 'txt':
return StreamSvgIcon.filetype_txt();
break;
case 'xls':
return StreamSvgIcon.filetype_xls();
break;
case 'xlsx':
return StreamSvgIcon.filetype_xlsx();
break;
case 'zip':
return StreamSvgIcon.filetype_zip();
break;
default:
return StreamSvgIcon.filetype_Generic();
break;
}
}
}
+180 -80
View File
@@ -658,6 +658,34 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildFilePickerSection() {
var _attachmentContainsFile =
_attachments.any((element) => element?.attachment?.type == 'file');
Color _getIconColor(int index) {
switch (index) {
case 0:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 1:
return !_attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 2:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
case 3:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
break;
}
}
return AnimatedContainer(
duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
height: _openFilePickerSection ? _filePickerSize : 0,
@@ -671,49 +699,49 @@ class MessageInputState extends State<MessageInput> {
IconButton(
iconSize: 24,
icon: StreamSvgIcon.pictures(
color: _filePickerIndex == 0
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
color: _getIconColor(0),
),
onPressed: () {
setState(() {
_filePickerIndex = 0;
});
},
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
),
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
color: _filePickerIndex == 1
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
color: _getIconColor(1),
),
onPressed: () {
pickFile(DefaultAttachmentTypes.file, false);
},
onPressed: !_attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.file, false);
},
),
IconButton(
iconSize: 24,
icon: StreamSvgIcon.camera(
color: _filePickerIndex == 2
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
color: _getIconColor(2),
),
onPressed: () {
pickFile(DefaultAttachmentTypes.image, true);
},
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.image, true);
},
),
IconButton(
padding: const EdgeInsets.all(0),
iconSize: 24,
icon: StreamSvgIcon.record(
color: _filePickerIndex == 3
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.5),
color: _getIconColor(3),
),
onPressed: () {
pickFile(DefaultAttachmentTypes.video, true);
},
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.video, true);
},
),
],
),
@@ -770,6 +798,9 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildPickerSection() {
var _attachmentContainsFile =
_attachments.any((element) => element.attachment.type == 'file');
switch (_filePickerIndex) {
case 0:
return FutureBuilder<bool>(
@@ -782,19 +813,22 @@ class MessageInputState extends State<MessageInput> {
}
if (snapshot.data) {
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);
});
}
},
return IgnorePointer(
ignoring: _attachmentContainsFile,
child: 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);
});
}
},
),
);
}
@@ -850,7 +884,7 @@ class MessageInputState extends State<MessageInput> {
);
if (file.size > _kMaxAttachmentSize) {
if (medium.type == AssetType.video) {
if (medium?.type == AssetType.video) {
final mediaInfo = await CompressVideoService.compressVideo(file.path);
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
@@ -890,7 +924,7 @@ class MessageInputState extends State<MessageInput> {
..file = file
..attachment = Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null,
type: medium.type == AssetType.image ? 'image' : 'video',
type: medium?.type == AssetType.image ? 'image' : 'video',
);
});
@@ -1053,7 +1087,7 @@ class MessageInputState extends State<MessageInput> {
offset: rejoin.length,
),
);
_debounce.cancel();
_mentionsOverlay?.remove();
_mentionsOverlay = null;
},
@@ -1201,44 +1235,98 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachments() {
return _attachments.isEmpty
? Container()
: LimitedBox(
maxHeight: 104.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: _attachments
.map(
(attachment) => Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Container(
height: 104,
width: 104,
child: _buildAttachment(attachment),
),
),
_buildRemoveButton(attachment),
attachment.uploaded
? SizedBox()
: Positioned.fill(
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
: Column(
children: [
if (_attachments.any((e) => e.attachment?.type == 'file'))
LimitedBox(
maxHeight: 73.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: _attachments
.where((e) => e.attachment?.type == 'file')
.map(
(e) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias,
child: FileAttachment(
attachment: e.attachment,
size: Size(
MediaQuery.of(context).size.width * 0.55,
MediaQuery.of(context).size.height * 0.3,
),
trailing: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
child: CircleAvatar(
backgroundColor:
Colors.black.withOpacity(0.2),
maxRadius: 12.0,
child: StreamSvgIcon.close(
color: Colors.white,
),
),
onTap: () {
setState(() {
_attachments.remove(e);
});
},
),
],
),
),
),
)
.toList(),
),
),
),
),
),
)
.toList(),
),
),
if (_attachments.any((e) => e.attachment?.type != 'file'))
LimitedBox(
maxHeight: 104.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: _attachments
.where((e) => e.attachment?.type != 'file')
.map(
(attachment) => Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias,
child: Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Container(
height: 104,
width: 104,
child: _buildAttachment(attachment),
),
),
_buildRemoveButton(attachment),
attachment.uploaded
? SizedBox()
: Positioned.fill(
child: Center(
child: Padding(
padding:
const EdgeInsets.all(16.0),
child:
CircularProgressIndicator(),
),
),
),
],
),
),
),
)
.toList(),
),
),
],
);
}
@@ -1275,9 +1363,9 @@ class MessageInputState extends State<MessageInput> {
Widget _buildAttachment(_SendingAttachment attachment) {
if (widget.attachmentThumbnailBuilders
?.containsKey(attachment.attachment.type) ==
?.containsKey(attachment.attachment?.type) ==
true) {
return widget.attachmentThumbnailBuilders[attachment.attachment.type](
return widget.attachmentThumbnailBuilders[attachment.attachment?.type](
context,
attachment,
);
@@ -1287,7 +1375,7 @@ class MessageInputState extends State<MessageInput> {
return SizedBox();
}
switch (attachment.attachment.type) {
switch (attachment.attachment?.type) {
case 'image':
case 'giphy':
return attachment.file != null
@@ -1562,12 +1650,24 @@ class MessageInputState extends State<MessageInput> {
attachmentType = mimeType.type;
}
Map<String, dynamic> extraDataMap = {};
if (mimeType?.subtype != null) {
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
}
if (file.size != null) {
extraDataMap['file_size'] = file.size;
}
final channel = StreamChannel.of(context).channel;
final attachment = _SendingAttachment(
file: file,
attachment: Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null,
type: attachmentType,
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
title: file.name ?? 'File',
),
);
+6 -1
View File
@@ -617,7 +617,12 @@ class _MessageListViewState extends State<MessageListView> {
showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap,
attachmentBorderRadiusGeometry: BorderRadius.circular(16),
attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
attachmentPadding: const EdgeInsets.all(2),
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
+81 -73
View File
@@ -223,6 +223,13 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.message.attachments?.any((element) => element.type == 'giphy') ==
true;
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
final hasFiles =
widget.message.attachments?.any((element) => element.type == 'file') ==
true;
return Portal(
child: Padding(
padding: widget.padding ?? EdgeInsets.all(8),
@@ -295,18 +302,46 @@ class _MessageWidgetState extends State<MessageWidget> {
messageTheme: widget.messageTheme,
),
)
: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
..._parseAttachments(context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: widget.borderSide ??
BorderSide(
color: Theme.of(context)
.brightness ==
Brightness
.dark
? Colors.white
.withAlpha(24)
: Colors.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
..._parseAttachments(context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
),
),
),
),
if (widget.showReactionPickerIndicator)
@@ -629,46 +664,40 @@ class _MessageWidgetState extends State<MessageWidget> {
[];
}
Padding wrapAttachmentWidget(
Widget wrapAttachmentWidget(
BuildContext context,
Widget attachmentWidget, {
Attachment attachment,
}) {
final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return Padding(
padding: EdgeInsets.only(
bottom: 4,
),
child: GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
child: Material(
color: attachment?.type == 'giphy'
? Colors.white
: _getBackgroundColor(),
clipBehavior: Clip.hardEdge,
shape: attachmentShape,
child: Padding(
padding: widget.attachmentPadding,
child: Material(
clipBehavior: Clip.hardEdge,
shape: attachmentShape,
type: MaterialType.transparency,
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
getFailedMessageWidget(
context,
padding: const EdgeInsets.all(8.0),
),
attachmentWidget,
],
),
return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
child: Material(
color:
attachment?.type == 'giphy' ? Colors.white : _getBackgroundColor(),
clipBehavior: Clip.hardEdge,
shape: attachmentShape,
child: Padding(
padding: widget.attachmentPadding,
child: Material(
clipBehavior: Clip.hardEdge,
shape: attachmentShape,
type: MaterialType.transparency,
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
getFailedMessageWidget(
context,
padding: const EdgeInsets.all(8.0),
),
attachmentWidget,
],
),
),
),
@@ -798,26 +827,6 @@ class _MessageWidgetState extends State<MessageWidget> {
return SizedBox();
}
Widget _wrapTextInBubble({
BuildContext context,
Widget child,
}) {
return Material(
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24)
: Colors.black.withAlpha(24),
),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
),
color: _getBackgroundColor(),
child: child,
);
}
Widget _buildTextBubble(BuildContext context) {
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
@@ -858,13 +867,6 @@ class _MessageWidgetState extends State<MessageWidget> {
],
),
);
if (!isOnlyEmoji) {
child = _wrapTextInBubble(
context: context,
child: child,
);
}
return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
@@ -873,6 +875,9 @@ class _MessageWidgetState extends State<MessageWidget> {
}
Color _getBackgroundColor() {
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
if ((widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
@@ -885,6 +890,9 @@ class _MessageWidgetState extends State<MessageWidget> {
return Color(0xFFE9F2FF);
}
if (isOnlyEmoji) {
return Colors.transparent;
}
return widget.messageTheme.messageBackgroundColor;
}
+240
View File
@@ -338,6 +338,30 @@ class StreamSvgIcon extends StatelessWidget {
);
}
factory StreamSvgIcon.download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.cloud_download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_cloud_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.copy({
double size,
Color color,
@@ -481,4 +505,220 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
factory StreamSvgIcon.filetype_7z({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_7z.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_csv({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_CSV.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_doc({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOC.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_docx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOCX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_Generic({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_Generic.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_html({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_html.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_md({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_MD.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_odt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ODT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pdf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PDF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_ppt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pptx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPTX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rtf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RTF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_tar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_txt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TXT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xls({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLS.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xlsx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLSX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_zip({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ZIP.svg',
color: color,
width: size,
height: size,
);
}
}