[Async Attachment Upload] Initial implementation

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2021-02-16 21:21:15 +05:30
parent 628542f99b
commit 0a6948cf32
54 changed files with 2561 additions and 1328 deletions
@@ -0,0 +1,6 @@
export 'file_attachment.dart';
export 'giphy_attachment.dart';
export 'image_attachment.dart';
export 'video_attachment.dart';
export 'attachment_widget.dart'
show AttachmentError, AttachmentSource, AttachmentSourceX;
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_theme.dart';
import '../utils.dart';
class AttachmentTitle extends StatelessWidget {
const AttachmentTitle({
Key key,
@required this.attachment,
@required this.messageTheme,
}) : super(key: key);
final MessageTheme messageTheme;
final Attachment attachment;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (attachment.titleLink != null) {
launchURL(context, attachment.titleLink);
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
fontWeight: FontWeight.bold,
),
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
style: messageTheme.messageText,
),
],
),
),
);
}
}
@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
typedef FailedBuilder = Widget Function(BuildContext, String);
class AttachmentUploadStateBuilder extends StatelessWidget {
final Message message;
final Attachment attachment;
final FailedBuilder failedBuilder;
final WidgetBuilder successBuilder;
final InProgressBuilder inProgressBuilder;
const AttachmentUploadStateBuilder({
Key key,
@required this.message,
@required this.attachment,
this.failedBuilder,
this.successBuilder,
this.inProgressBuilder,
}) : assert(message != null),
assert(attachment != null),
super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.uploadState == null) return Offstage();
final messageId = message.id;
final attachmentId = attachment.id;
var inProgress = inProgressBuilder;
inProgress ??= (context, int sent, int total) {
return _InProgressState(
sent: sent,
total: total,
attachmentId: attachmentId,
);
};
var failed = failedBuilder;
failed ??= (context, error) {
return _FailedState(
error: error,
messageId: messageId,
attachmentId: attachmentId,
);
};
var success = successBuilder;
success ??= (context) => _SuccessState();
return attachment.uploadState.when(
inProgress: (sent, total) => inProgress(context, sent, total),
success: () => success(context),
failed: (error) => failed(context, error),
);
}
}
class _IconButton extends StatelessWidget {
final Widget icon;
final double iconSize;
final VoidCallback onPressed;
final Color fillColor;
const _IconButton({
Key key,
this.icon,
this.iconSize = 24.0,
this.onPressed,
this.fillColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: iconSize,
width: iconSize,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor:
fillColor ?? StreamChatTheme.of(context).colorTheme.overlayDark,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: icon,
),
);
}
}
class _InProgressState extends StatelessWidget {
final int sent;
final int total;
final String attachmentId;
const _InProgressState({
Key key,
@required this.sent,
@required this.total,
@required this.attachmentId,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_IconButton(
icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.white,
),
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
Center(
child: UploadProgressIndicator(
uploaded: sent,
total: total,
),
)
],
);
}
}
class _FailedState extends StatelessWidget {
final String error;
final String messageId;
final String attachmentId;
const _FailedState({
Key key,
this.error,
@required this.messageId,
@required this.attachmentId,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
final theme = StreamChatTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_IconButton(
icon: StreamSvgIcon.retry(
color: theme.colorTheme.white,
),
onPressed: () {
return channel.retryAttachmentUpload(messageId, attachmentId);
},
),
Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorTheme.overlayDark.withOpacity(0.6),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
child: Text(
'UPLOAD ERROR',
style: theme.textTheme.footnote.copyWith(
color: theme.colorTheme.white,
),
),
),
),
)
],
);
}
}
class _SuccessState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topRight,
child: CircleAvatar(
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
maxRadius: 12.0,
child: StreamSvgIcon.check(
color: StreamChatTheme.of(context).colorTheme.white,
),
),
);
}
}
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_theme.dart';
enum AttachmentSource {
local,
network,
}
extension AttachmentSourceX on AttachmentSource {
/// The [when] method is the equivalent to pattern matching.
/// Its prototype depends on the AttachmentSource defined.
T when<T>({
@required T Function() local,
@required T Function() network,
}) {
assert(() {
if (local == null || network == null) {
throw 'check for all possible cases';
}
return true;
}());
switch (this) {
case AttachmentSource.local:
return local();
case AttachmentSource.network:
return network();
}
}
}
abstract class AttachmentWidget extends StatelessWidget {
final Size size;
final Message message;
final Attachment attachment;
final AttachmentSource _source;
AttachmentSource get source => _source ?? attachment.file != null
? AttachmentSource.local
: AttachmentSource.network;
const AttachmentWidget({
Key key,
@required this.message,
@required this.attachment,
this.size,
AttachmentSource source,
}) : _source = source,
super(key: key);
}
class AttachmentError extends StatelessWidget {
final Size size;
const AttachmentError({
Key key,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: size?.width,
height: size?.height,
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
child: Center(
child: Icon(
Icons.error_outline,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
),
);
}
}
@@ -0,0 +1,255 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../upload_progress_indicator.dart';
import 'attachment_widget.dart';
class FileAttachment extends AttachmentWidget {
final Widget title;
final Widget trailing;
const FileAttachment({
Key key,
@required Message message,
@required Attachment attachment,
Size size,
this.title,
this.trailing,
}) : super(key: key, message: message, attachment: attachment, size: size);
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
@override
Widget build(BuildContext context) {
return Material(
child: Container(
width: size?.width ?? 100,
height: 56.0,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
child: _getFileTypeImage(context),
height: 40.0,
width: 33.33,
margin: EdgeInsets.all(8.0),
),
SizedBox(width: 8.0),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attachment?.title ?? 'File',
style: StreamChatTheme.of(context).textTheme.bodyBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 3.0),
_buildSubtitle(context),
],
),
),
SizedBox(width: 8.0),
_buildTrailing(context),
],
),
),
);
}
ShapeBorder _getDefaultShape(BuildContext context) {
return RoundedRectangleBorder(
side: BorderSide(width: 0.0, color: Colors.transparent),
borderRadius: BorderRadius.circular(8),
);
}
Widget _getFileTypeImage(BuildContext context) {
if (isImageAttachment) {
return Material(
clipBehavior: Clip.antiAlias,
type: MaterialType.transparency,
shape: _getDefaultShape(context),
child: source.when(
local: () => Image.memory(
attachment.file.bytes,
fit: BoxFit.cover,
errorBuilder: (_, obj, trace) {
return getFileTypeImage(attachment.extraData['other']);
},
),
network: () => CachedNetworkImage(
imageUrl: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
fit: BoxFit.cover,
errorWidget: (_, obj, trace) {
return getFileTypeImage(attachment.extraData['other']);
},
progressIndicatorBuilder: (context, _, progress) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: const CircularProgressIndicator(),
),
);
},
),
),
);
}
if (isVideoAttachment) {
return Material(
clipBehavior: Clip.antiAlias,
type: MaterialType.transparency,
shape: _getDefaultShape(context),
child: source.when(
local: () => VideoThumbnailImage(
video: attachment.file.path,
placeholderBuilder: (_) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: const CircularProgressIndicator(),
),
);
},
),
network: () => VideoThumbnailImage(
video: attachment.assetUrl,
placeholderBuilder: (_) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: const CircularProgressIndicator(),
),
);
},
),
),
);
}
return getFileTypeImage(attachment.extraData['mime_type']);
}
Widget _buildButton({
Widget icon,
double iconSize = 24.0,
VoidCallback onPressed,
Color fillColor,
}) {
return Container(
height: iconSize,
width: iconSize,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor: fillColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: icon,
),
);
}
Widget _buildTrailing(BuildContext context) {
final theme = StreamChatTheme.of(context);
final channel = StreamChannel.of(context).channel;
final attachmentId = attachment.id;
var trailingWidget = trailing;
trailingWidget ??= attachment.uploadState?.when(
inProgress: (_, __) => Padding(
padding: const EdgeInsets.all(8.0),
child: _buildButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
),
success: () => Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
backgroundColor: theme.colorTheme.accentBlue,
maxRadius: 12.0,
child: StreamSvgIcon.check(color: theme.colorTheme.white),
),
),
failed: (_) => Padding(
padding: const EdgeInsets.all(8.0),
child: _buildButton(
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.retryAttachmentUpload(
message?.id,
attachmentId,
),
),
),
) ??
IconButton(
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () {
launchURL(context, attachment.assetUrl);
},
);
return Material(
type: MaterialType.transparency,
child: trailingWidget,
);
}
Widget _buildSubtitle(BuildContext context) {
final theme = StreamChatTheme.of(context);
final size = attachment.file?.size ?? attachment.extraData['file_size'];
final textStyle = theme.textTheme.footnote.copyWith(
color: theme.colorTheme.grey,
);
return attachment.uploadState?.when(
inProgress: (sent, total) {
return UploadProgressIndicator(
uploaded: sent,
total: total,
showBackground: false,
padding: EdgeInsets.zero,
textStyle: textStyle,
progressIndicatorColor: theme.colorTheme.accentBlue,
);
},
success: () {
return Text(
'${fileSize(size, 1)}/${fileSize(size, 1)}',
style: textStyle,
);
},
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
) ??
Text('${fileSize(size)}', style: textStyle);
}
}
@@ -0,0 +1,398 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../full_screen_media.dart';
import '../stream_chat_theme.dart';
import '../stream_svg_icon.dart';
import 'attachment_widget.dart';
class GiphyAttachment extends AttachmentWidget {
final MessageTheme messageTheme;
final ShowMessageCallback onShowMessage;
final ValueChanged<ReturnActionType> onReturnAction;
const GiphyAttachment({
Key key,
@required Message message,
@required Attachment attachment,
Size size,
this.messageTheme,
this.onShowMessage,
this.onReturnAction,
}) : super(key: key, message: message, attachment: attachment, size: size);
@override
Widget build(BuildContext context) {
final imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
if (imageUrl == null && source == AttachmentSource.network) {
return AttachmentError();
}
if (attachment.actions != null) {
return _buildSendingAttachment(context, imageUrl);
}
return _buildSentAttachment(context, imageUrl);
}
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
final streamChannel = StreamChannel.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
color: StreamChatTheme.of(context).colorTheme.white,
elevation: 2,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
bottomRight: Radius.circular(0.0),
topLeft: Radius.circular(16.0),
bottomLeft: Radius.circular(16.0),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () => _onImageTap(context),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) {
return AttachmentError(size: size);
},
fit: BoxFit.cover,
),
),
),
),
Positioned(
bottom: 16,
left: 16,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color:
StreamChatTheme.of(context).colorTheme.white,
size: 16,
),
Text(
'GIPHY',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
),
),
],
),
if (attachment.title != null)
Container(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
color: Colors.white,
elevation: 2,
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.left(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
shape: CircleBorder(),
),
Expanded(
child: Center(
child: Text(
'"${attachment.title}"',
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
Card(
color: Colors.white,
elevation: 2,
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.right(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
shape: CircleBorder(),
),
],
),
),
),
SizedBox(
height: 4.0,
),
Container(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
width: double.infinity,
height: 0.5,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: FlatButton(
height: 50,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'cancel',
});
},
child: Text(
'Cancel',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
height: 50.0,
),
Expanded(
child: FlatButton(
height: 50,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'send',
});
},
child: Text(
'Send',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
fontWeight: FontWeight.bold),
),
),
),
],
),
],
),
),
SizedBox(
height: 4.0,
),
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 16.0,
),
SizedBox(
width: 8.0,
),
Text(
'Only visible to you',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
),
],
),
),
),
],
);
}
void _onImageTap(BuildContext context) async {
final res = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [attachment],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
},
),
);
if (res != null) onReturnAction(res);
}
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
return Container(
child: GestureDetector(
onTap: () async {
final res =
await Navigator.push(context, MaterialPageRoute(builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [attachment],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
}));
if (res != null) onReturnAction(res);
},
child: Stack(
children: [
CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) {
return AttachmentError(size: size);
},
fit: BoxFit.cover,
),
Positioned(
bottom: 8,
left: 8,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).colorTheme.white,
size: 16,
),
Text(
'GIPHY',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,139 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'attachment_title.dart';
import '../full_screen_media.dart';
import '../stream_chat_theme.dart';
import 'attachment_widget.dart';
class ImageAttachment extends AttachmentWidget {
final MessageTheme messageTheme;
final bool showTitle;
final ShowMessageCallback onShowMessage;
final ValueChanged<ReturnActionType> onReturnAction;
final VoidCallback onAttachmentTap;
const ImageAttachment({
Key key,
@required Message message,
@required Attachment attachment,
Size size,
this.messageTheme,
this.showTitle = false,
this.onShowMessage,
this.onReturnAction,
this.onAttachmentTap,
}) : super(key: key, message: message, attachment: attachment, size: size);
@override
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.localUri == null) {
return AttachmentError(size: size);
}
return _buildImageAttachment(
context,
Image.memory(
attachment.file.bytes,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (context, _, __) {
return Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
},
),
);
},
network: () {
final imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
if (imageUrl == null) {
return AttachmentError(size: size);
}
return _buildImageAttachment(
context,
CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) {
return AttachmentError(size: size);
},
fit: BoxFit.cover,
),
);
},
);
}
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
return ConstrainedBox(
constraints: BoxConstraints.loose(size),
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: [
GestureDetector(
onTap: onAttachmentTap ??
() async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [attachment],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
},
),
);
if (result != null) onReturnAction(result);
},
child: imageWidget,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: AttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
),
],
),
),
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
);
}
}
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'attachment_title.dart';
import 'attachment_upload_state_builder.dart';
import 'attachment_widget.dart';
class VideoAttachment extends AttachmentWidget {
final MessageTheme messageTheme;
final ShowMessageCallback onShowMessage;
final ValueChanged<ReturnActionType> onReturnAction;
const VideoAttachment({
Key key,
@required Message message,
@required Attachment attachment,
Size size,
this.messageTheme,
this.onShowMessage,
this.onReturnAction,
}) : super(key: key, message: message, attachment: attachment, size: size);
@override
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.file == null) {
return AttachmentError(size: size);
}
return _buildVideoAttachment(
context,
VideoThumbnailImage(
video: attachment.file.path,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(size: size),
),
);
},
network: () {
if (attachment.assetUrl == null) {
return AttachmentError(size: size);
}
return _buildVideoAttachment(
context,
VideoThumbnailImage(
video: attachment.assetUrl,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(size: size),
),
);
},
);
}
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) {
return ConstrainedBox(
constraints: BoxConstraints.loose(size),
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () async {
final channel = StreamChannel.of(context).channel;
final res = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [attachment],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
),
),
);
if (res != null) onReturnAction(res);
},
child: Stack(
children: [
Container(
height: size?.height,
width: size?.width,
child: videoWidget,
),
Center(
child: Material(
shape: CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Icon(Icons.play_arrow),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: AttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
),
],
),
),
),
if (attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
);
}
}