[Async Attachment Upload] Initial implementation
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -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;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class AttachmentTitle extends StatelessWidget {
|
||||
const AttachmentTitle({
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+52
-75
@@ -1,48 +1,42 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import '../full_screen_media.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../stream_svg_icon.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class GiphyAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
class GiphyAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const GiphyAttachment({
|
||||
Key key,
|
||||
this.attachment,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
final imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
if (imageUrl == null && source == AttachmentSource.network) {
|
||||
return AttachmentError();
|
||||
}
|
||||
|
||||
return attachment.actions != null
|
||||
? _buildSendingAttachment(context)
|
||||
: _buildSentAttachment(context);
|
||||
if (attachment.actions != null) {
|
||||
return _buildSendingAttachment(context, imageUrl);
|
||||
}
|
||||
return _buildSentAttachment(context, imageUrl);
|
||||
}
|
||||
|
||||
Widget _buildSendingAttachment(context) {
|
||||
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -67,9 +61,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_onImageTap(context);
|
||||
},
|
||||
onTap: () => _onImageTap(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
@@ -87,13 +79,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -305,44 +294,38 @@ class GiphyAttachment extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onImageTap(BuildContext context) async {
|
||||
var 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);
|
||||
}
|
||||
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(context) {
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
||||
return Container(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var res =
|
||||
final res =
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
@@ -350,10 +333,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
if (res != null) {
|
||||
onReturnAction(res);
|
||||
}
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -369,13 +349,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
|
||||
const AttachmentError({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.localUri != null) {
|
||||
return Image.file(
|
||||
File(attachment.localUri.path),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height ?? 200,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelFileDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
@@ -164,6 +166,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FileAttachment(
|
||||
message: media.values.toList()[position],
|
||||
attachment: media.keys.toList()[position],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelMediaDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class ICompressVideoService {
|
||||
static final ICompressVideoService instance = ICompressVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
ICompressVideoService._();
|
||||
|
||||
Future<MediaInfo> compress(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ICompressVideoService get compressVideoService =>
|
||||
ICompressVideoService.instance;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
|
||||
@@ -23,16 +23,6 @@ extension StringExtension on String {
|
||||
if (characters.length > 3) return false;
|
||||
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
|
||||
@@ -43,3 +33,14 @@ extension IterableX<T> on Iterable<T> {
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
|
||||
///
|
||||
extension PlatformFileX on PlatformFile {
|
||||
///
|
||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||
path: path,
|
||||
name: name,
|
||||
bytes: bytes,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
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: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_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'media_utils.dart';
|
||||
|
||||
enum FileAttachmentType { local, online }
|
||||
|
||||
class FileAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
final Widget trailing;
|
||||
final FileAttachmentType attachmentType;
|
||||
final PlatformFile file;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
this.trailing,
|
||||
this.attachmentType = FileAttachmentType.online,
|
||||
this.file,
|
||||
}) : 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
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: widget.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(),
|
||||
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(
|
||||
widget.attachment?.title ?? 'File',
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 3.0),
|
||||
Text(
|
||||
'${filesize(widget.attachment.extraData['file_size'])}',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, widget.attachment.assetUrl);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage() {
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title)?.type == 'image')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return Image.memory(
|
||||
widget.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.attachment.imageUrl ??
|
||||
widget.attachment.assetUrl ??
|
||||
widget.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -48,36 +51,34 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
|
||||
int _currentPage;
|
||||
|
||||
List<VideoPackage> videoPackages = [];
|
||||
final videoPackages = <String, VideoPackage>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
_pageController = PageController(initialPage: widget.startIndex);
|
||||
_currentPage = widget.startIndex;
|
||||
widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList()
|
||||
.forEach((element) {
|
||||
videoPackages.add(VideoPackage(
|
||||
context,
|
||||
element,
|
||||
() {
|
||||
setState(() {});
|
||||
},
|
||||
showControls: true,
|
||||
));
|
||||
});
|
||||
for (final attachment in widget.mediaAttachments) {
|
||||
if (attachment.type != 'video') continue;
|
||||
final package = VideoPackage(attachment, showControls: true);
|
||||
videoPackages[attachment.id] = package;
|
||||
}
|
||||
initializePlayers();
|
||||
}
|
||||
|
||||
Future<void> initializePlayers() async {
|
||||
await Future.wait(videoPackages.values.map(
|
||||
(it) => it.initialize(),
|
||||
));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
@@ -92,14 +93,18 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
_currentPage = val;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, position) {
|
||||
if (widget.mediaAttachments[position].type == 'image' ||
|
||||
widget.mediaAttachments[position].type == 'giphy') {
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
if (attachment.type == 'image' ||
|
||||
attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
widget.mediaAttachments[position].imageUrl ??
|
||||
widget.mediaAttachments[position].assetUrl ??
|
||||
widget.mediaAttachments[position].thumbUrl),
|
||||
imageProvider:
|
||||
imageUrl == null && attachment.localUri != null
|
||||
? Image.memory(attachment.file.bytes).image
|
||||
: CachedNetworkImageProvider(imageUrl),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
@@ -125,12 +130,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (widget.mediaAttachments[position].type ==
|
||||
'video') {
|
||||
var controllerPackage = videoPackages[videoAttachments
|
||||
.indexOf(widget.mediaAttachments[position])];
|
||||
|
||||
if (!controllerPackage.initialised) {
|
||||
} else if (attachment.type == 'video') {
|
||||
final controller = videoPackages[attachment.id];
|
||||
if (!controller.initialized) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
@@ -151,7 +153,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
vertical: 50.0,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
controller: controller.chewieController,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -188,7 +190,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
mediaAttachments: widget.mediaAttachments,
|
||||
message: widget.message,
|
||||
videoPackages: videoPackages,
|
||||
mediaSelectedCallBack: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
@@ -224,54 +225,58 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoPackages.forEach((element) {
|
||||
element.dispose();
|
||||
});
|
||||
void dispose() async {
|
||||
for (final package in videoPackages.values) {
|
||||
await package.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPackage {
|
||||
VideoPlayerController _videoPlayerController;
|
||||
final bool _showControls;
|
||||
final bool _autoInitialize;
|
||||
final VideoPlayerController _videoPlayerController;
|
||||
ChewieController _chewieController;
|
||||
bool initialised = false;
|
||||
VoidCallback onInit;
|
||||
BuildContext context;
|
||||
bool showControls;
|
||||
|
||||
///
|
||||
VideoPackage(this.context, Attachment attachment, this.onInit,
|
||||
{this.showControls = false}) {
|
||||
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
|
||||
_videoPlayerController.initialize().whenComplete(() {
|
||||
initialised = true;
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: true,
|
||||
showControls: showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
onInit();
|
||||
});
|
||||
|
||||
VoidCallback errorListener;
|
||||
errorListener = () {
|
||||
if (_videoPlayerController.value.hasError) {
|
||||
Navigator.pop(context);
|
||||
launchURL(context, attachment.titleLink);
|
||||
}
|
||||
_videoPlayerController.removeListener(errorListener);
|
||||
};
|
||||
_videoPlayerController.addListener(errorListener);
|
||||
}
|
||||
|
||||
VideoPlayerController get videoPlayer => _videoPlayerController;
|
||||
|
||||
ChewieController get chewieController => _chewieController;
|
||||
|
||||
void dispose() {
|
||||
_videoPlayerController.dispose();
|
||||
_chewieController.dispose();
|
||||
bool get initialized => _videoPlayerController.value.initialized;
|
||||
|
||||
VideoPackage(
|
||||
Attachment attachment, {
|
||||
bool showControls = false,
|
||||
bool autoInitialize = true,
|
||||
}) : assert(attachment != null),
|
||||
_showControls = showControls,
|
||||
_autoInitialize = autoInitialize,
|
||||
_videoPlayerController = attachment.localUri != null
|
||||
? VideoPlayerController.file(File.fromUri(attachment.localUri))
|
||||
: VideoPlayerController.network(attachment.assetUrl);
|
||||
|
||||
Future<void> initialize() {
|
||||
return _videoPlayerController.initialize().then((_) {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: _autoInitialize,
|
||||
showControls: _showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener(VoidCallback listener) {
|
||||
return _videoPlayerController.addListener(listener);
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback listener) {
|
||||
return _videoPlayerController.removeListener(listener);
|
||||
}
|
||||
|
||||
Future<void> dispose() {
|
||||
_chewieController?.dispose();
|
||||
return _videoPlayerController?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ class ImageActionsModal extends StatelessWidget {
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
StreamChannel.of(context)
|
||||
.channel
|
||||
.deleteMessage(message);
|
||||
},
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment_title.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class ImageAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.messageTheme,
|
||||
this.showTitle = true,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var 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: CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showTitle &&
|
||||
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => launchURL(
|
||||
context,
|
||||
attachment.titleLink ?? attachment.ogScrapeUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -28,7 +28,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
final List<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final List<VideoPackage> videoPackages;
|
||||
final ValueChanged<int> mediaSelectedCallBack;
|
||||
|
||||
/// Creates a channel header
|
||||
@@ -41,7 +40,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.totalPages = 0,
|
||||
this.mediaAttachments,
|
||||
this.message,
|
||||
this.videoPackages,
|
||||
this.mediaSelectedCallBack,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
@@ -214,17 +212,14 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
itemBuilder: (context, index) {
|
||||
Widget media;
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
|
||||
if (attachment.type == 'video') {
|
||||
var controllerPackage = widget.videoPackages[
|
||||
videoAttachments.indexOf(attachment)];
|
||||
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack(index),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file?.path ??
|
||||
attachment.assetUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
@@ -9,12 +8,14 @@ class ImageGroup extends StatelessWidget {
|
||||
Key key,
|
||||
@required this.images,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
@required this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Attachment> images;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@@ -129,14 +130,12 @@ class ImageGroup extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onTap(context, index),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: images[index].imageUrl ??
|
||||
images[index].thumbUrl ??
|
||||
images[index].assetUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
return ImageAttachment(
|
||||
attachment: images[index],
|
||||
size: size,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
onAttachmentTap: () => _onTap(context, index),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MediaUtils {
|
||||
static 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;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@@ -54,7 +53,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
this.messageShape,
|
||||
this.attachmentShape,
|
||||
this.reverse = false,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -182,7 +180,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
showSendingIndicator: false,
|
||||
shape: widget.messageShape,
|
||||
attachmentShape: widget.attachmentShape,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
@@ -298,10 +295,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
if (answer) {
|
||||
try {
|
||||
Navigator.pop(context);
|
||||
await StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
||||
} catch (err) {
|
||||
_showErrorAlert();
|
||||
}
|
||||
@@ -570,10 +564,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final client = StreamChat.of(context).client;
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (isUpdateFailed) {
|
||||
client.updateMessage(widget.message, channel.cid);
|
||||
channel.updateMessage(widget.message);
|
||||
} else {
|
||||
channel.sendMessage(widget.message);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -11,7 +11,7 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/compress_video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
@@ -19,16 +19,16 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_uploader.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'quoted_message_widget.dart';
|
||||
import 'video_thumbnail_image.dart';
|
||||
|
||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
_SendingAttachment,
|
||||
Attachment,
|
||||
);
|
||||
|
||||
enum ActionsLocation {
|
||||
@@ -44,7 +44,7 @@ enum DefaultAttachmentTypes {
|
||||
|
||||
const _kMinMediaPickerSize = 360.0;
|
||||
|
||||
const _kMaxAttachmentSize = 20480; //20MB
|
||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
|
||||
/// Inactive state
|
||||
/// 
|
||||
@@ -99,7 +99,6 @@ class MessageInput extends StatefulWidget {
|
||||
this.maxHeight = 150,
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.disableAttachments = false,
|
||||
this.attachmentUploader,
|
||||
this.initialMessage,
|
||||
this.textEditingController,
|
||||
this.actions,
|
||||
@@ -135,9 +134,6 @@ class MessageInput extends StatefulWidget {
|
||||
/// If true the attachments button will not be displayed
|
||||
final bool disableAttachments;
|
||||
|
||||
/// A delegate to upload attachments
|
||||
final AttachmentUploader attachmentUploader;
|
||||
|
||||
/// The text controller of the TextField
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
@@ -178,7 +174,7 @@ class MessageInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class MessageInputState extends State<MessageInput> {
|
||||
final _attachments = <String, _SendingAttachment>{};
|
||||
final _attachments = <String, Attachment>{};
|
||||
final List<User> _mentionedUsers = [];
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
@@ -204,8 +200,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
AttachmentUploader _attachmentUploader;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -391,8 +385,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedCrossFade(
|
||||
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
||||
_attachments.values.every((a) => a.isUploaded == true))
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _buildSendButton(context),
|
||||
@@ -786,7 +779,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildFilePickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
@@ -943,7 +936,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
@@ -1045,98 +1038,47 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
final attachmentId = medium.id;
|
||||
_attachments[attachmentId] = _SendingAttachment(id: attachmentId);
|
||||
try {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
var file = PlatformFile(
|
||||
path: mediaFile.path,
|
||||
size: ((await mediaFile.length()) / 1024).ceil(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachmentId);
|
||||
return;
|
||||
}
|
||||
file = PlatformFile(
|
||||
name: file.name,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_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. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: medium?.type == AssetType.image ? 'image' : 'video',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
final fileType = medium.type == AssetType.image
|
||||
? DefaultAttachmentTypes.image
|
||||
: 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) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||
});
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
}
|
||||
|
||||
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(() {
|
||||
_attachments[medium.id] = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildCommandIcon(String iconType) {
|
||||
@@ -1561,10 +1503,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildAttachments() {
|
||||
if (_attachments.isEmpty) return Offstage();
|
||||
final fileAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType == 'file')
|
||||
.where((it) => it.type == 'file')
|
||||
.toList(growable: false);
|
||||
final remainingAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType != 'file')
|
||||
.where((it) => it.type != 'file')
|
||||
.toList(growable: false);
|
||||
return Column(
|
||||
children: [
|
||||
@@ -1582,37 +1524,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FileAttachment(
|
||||
attachment: e.attachment,
|
||||
attachmentType: FileAttachmentType.local,
|
||||
file: e.file,
|
||||
message: null,
|
||||
attachment: e,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.65,
|
||||
56.0,
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.6),
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _attachments.remove(e.id));
|
||||
},
|
||||
),
|
||||
child: _buildRemoveButton(e),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.insertBetween(const SizedBox(width: 8)),
|
||||
.insertBetween(const SizedBox(height: 8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1638,16 +1563,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: _buildAttachment(attachment),
|
||||
),
|
||||
),
|
||||
_buildRemoveButton(attachment),
|
||||
if (!attachment.isUploaded)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _buildRemoveButton(attachment),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1660,44 +1580,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadProgressIndicator(int uploaded, int total) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget _buildRemoveButton(Attachment attachment) {
|
||||
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) {
|
||||
return Positioned(
|
||||
height: 24,
|
||||
width: 24,
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -1721,21 +1607,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachment(_SendingAttachment attachment) {
|
||||
if (widget.attachmentThumbnailBuilders
|
||||
?.containsKey(attachment.attachmentType) ==
|
||||
Widget _buildAttachment(Attachment attachment) {
|
||||
if (attachment == null) return Offstage();
|
||||
|
||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
||||
true) {
|
||||
return widget.attachmentThumbnailBuilders[attachment.attachmentType](
|
||||
return widget.attachmentThumbnailBuilders[attachment.type](
|
||||
context,
|
||||
attachment,
|
||||
);
|
||||
}
|
||||
|
||||
if (attachment.attachment == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
switch (attachment.attachmentType) {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
case 'giphy':
|
||||
return attachment.file != null
|
||||
@@ -1749,32 +1632,33 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.network(
|
||||
attachment.attachment.imageUrl,
|
||||
: 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'video':
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
child: FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(attachment.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,
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: 104,
|
||||
width: 104,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -1787,7 +1671,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return Container(
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
@@ -1953,11 +1836,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
/// Use this to add custom type attachments
|
||||
void addAttachment(Attachment attachment) {
|
||||
setState(() {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1966,7 +1847,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||
setState(() => _inputEnabled = false);
|
||||
|
||||
PlatformFile file;
|
||||
AttachmentFile file;
|
||||
String attachmentType;
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
@@ -1988,8 +1869,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = PlatformFile(
|
||||
size: (bytes.length / 1024).ceil(),
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
@@ -2007,7 +1888,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
withData: true,
|
||||
);
|
||||
if (res?.files?.isNotEmpty == true) {
|
||||
file = res.files.single;
|
||||
file = res.files.single.toAttachmentFile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2017,7 +1898,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
final mimeType = file.path.split('/').last.mimeType;
|
||||
|
||||
var extraDataMap = <String, dynamic>{};
|
||||
final extraDataMap = <String, dynamic>{};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
@@ -2035,97 +1916,46 @@ class MessageInputState extends State<MessageInput> {
|
||||
extraDataMap['file_size'] = file.size;
|
||||
}
|
||||
|
||||
final attachment = _SendingAttachment(
|
||||
final attachment = Attachment(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
title: file.name,
|
||||
),
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
);
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
setState(() => _attachments[attachmentId] = attachment);
|
||||
_attachments[attachment.id] = attachment;
|
||||
|
||||
if (file.size / 1024 > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'video') {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
file = PlatformFile(
|
||||
name: mediaInfo.title,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'Video') {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachment.id);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) => it.copyWith(file: file));
|
||||
});
|
||||
} else {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _uploadAttachment(
|
||||
PlatformFile file,
|
||||
DefaultAttachmentTypes type, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) {
|
||||
if (type == DefaultAttachmentTypes.image) {
|
||||
return _attachmentUploader.uploadImage(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
} else {
|
||||
return _attachmentUploader.uploadFile(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
_attachments.update(attachment.id, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
extraData: {...it.extraData}..update('file_size', (_) => file.size),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
@@ -2203,7 +2033,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (widget.editMessage != null) {
|
||||
message = widget.editMessage.copyWith(
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
);
|
||||
@@ -2211,7 +2041,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
message = (widget.initialMessage ?? Message()).copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
||||
@@ -2237,13 +2067,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
_mentionedUsers.clear();
|
||||
|
||||
if (widget.editMessage == null ||
|
||||
widget.editMessage.status == MessageSendingStatus.failed) {
|
||||
widget.editMessage.status == MessageSendingStatus.failed ||
|
||||
widget.editMessage.status == MessageSendingStatus.sending) {
|
||||
sendingFuture = channel.sendMessage(message);
|
||||
} else {
|
||||
sendingFuture = StreamChat.of(context).client.updateMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
sendingFuture = channel.updateMessage(message);
|
||||
}
|
||||
|
||||
return sendingFuture.then((resp) {
|
||||
@@ -2253,12 +2081,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
|
||||
return attachments.map((attachment) {
|
||||
return attachment.attachment;
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription _keyboardListener;
|
||||
|
||||
void _showErrorAlert(String description) {
|
||||
@@ -2334,16 +2156,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
void _parseExistingMessage(Message message) {
|
||||
textEditingController.text = message.text;
|
||||
|
||||
_messageIsPresent = true;
|
||||
|
||||
message.attachments?.forEach((attachment) {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
for (final attachment in message.attachments) {
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2363,63 +2181,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
class _SendingAttachment {
|
||||
_SendingAttachment({
|
||||
String id,
|
||||
this.file,
|
||||
this.attachment,
|
||||
this.totalUploaded = 0,
|
||||
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.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Returns the first item of the tuple
|
||||
|
||||
@@ -239,46 +239,50 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final MessageListController _messageListController = MessageListController();
|
||||
|
||||
final Map<String, VideoPackage> videoPackages = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
print('Getting popped');
|
||||
return false;
|
||||
},
|
||||
child: MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -778,7 +782,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -939,10 +942,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
|
||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
||||
if (!message.isDeleted &&
|
||||
!message.isSystem &&
|
||||
!message.isEphemeral &&
|
||||
widget.onMessageSwiped != null) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
@@ -1056,7 +1061,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
streamChannel.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
videoPackages.values.forEach((e) => e.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final void Function(User) onUserAvatarTap;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageReactionsModal({
|
||||
Key key,
|
||||
@@ -37,7 +36,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
this.reverse = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.onUserAvatarTap,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -146,7 +144,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
(message.status ==
|
||||
MessageSendingStatus.sent ||
|
||||
message.status == null),
|
||||
videoPackages: videoPackages,
|
||||
),
|
||||
),
|
||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_group.dart';
|
||||
import 'message_text.dart';
|
||||
@@ -142,9 +143,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
/// The cache for the video controllers of attachments IDed as message ID + attachment index
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -193,7 +191,6 @@ class MessageWidget extends StatefulWidget {
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
this.videoPackages,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -236,6 +233,7 @@ class MessageWidget extends StatefulWidget {
|
||||
},
|
||||
'file': (context, message, attachment) {
|
||||
return FileAttachment(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
@@ -250,7 +248,8 @@ class MessageWidget extends StatefulWidget {
|
||||
_MessageWidgetState createState() => _MessageWidgetState();
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
class _MessageWidgetState extends State<MessageWidget>
|
||||
with AutomaticKeepAliveClientMixin<MessageWidget> {
|
||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||
|
||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||
@@ -298,8 +297,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showSendingIndicator ||
|
||||
isDeleted;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.message.attachments?.isNotEmpty == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||
var leftPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||
@@ -744,7 +747,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -773,7 +775,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onThreadTap: widget.onThreadTap,
|
||||
showReactions: widget.showReactions,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -824,6 +825,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
),
|
||||
images: images,
|
||||
message: widget.message,
|
||||
messageTheme: widget.messageTheme,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
@@ -838,41 +840,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
children: widget.message.attachments
|
||||
?.where((element) => element.ogScrapeUrl == null)
|
||||
?.map((attachment) {
|
||||
if (attachment.type == 'video') {
|
||||
VideoPackage package;
|
||||
|
||||
if (widget.videoPackages == null) {
|
||||
package = VideoPackage(context, attachment, () {});
|
||||
} else {
|
||||
package = widget?.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] ??
|
||||
VideoPackage(context, attachment, () {});
|
||||
}
|
||||
|
||||
if (widget.videoPackages != null) {
|
||||
widget.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] =
|
||||
package;
|
||||
}
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: VideoAttachment(
|
||||
attachment: attachment,
|
||||
messageTheme: widget.messageTheme,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
onReturnAction: widget.onReturnAction,
|
||||
videoPackage: package,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final attachmentBuilder =
|
||||
widget.attachmentBuilders[attachment.type];
|
||||
|
||||
@@ -885,7 +852,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return wrapAttachmentWidget(
|
||||
context,
|
||||
attachmentWidget,
|
||||
attachment: attachment,
|
||||
);
|
||||
})?.insertBetween(SizedBox(
|
||||
height: widget.attachmentPadding.vertical / 2,
|
||||
@@ -897,9 +863,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget wrapAttachmentWidget(
|
||||
BuildContext context,
|
||||
Widget attachmentWidget, {
|
||||
Attachment attachment,
|
||||
}) {
|
||||
Widget attachmentWidget,
|
||||
) {
|
||||
final attachmentShape =
|
||||
widget.attachmentShape ?? _getDefaultAttachmentShape(context);
|
||||
return Material(
|
||||
@@ -930,8 +895,29 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
final style = widget.messageTheme.createdAt;
|
||||
final message = widget.message;
|
||||
|
||||
if (hasNonUrlAttachments &&
|
||||
(message.status == MessageSendingStatus.sending ||
|
||||
message.status == MessageSendingStatus.updating)) {
|
||||
final totalAttachments = message.attachments.length;
|
||||
final uploadRemaining = message.attachments.where((it) {
|
||||
return !it.uploadState.isSuccess;
|
||||
}).length;
|
||||
if (uploadRemaining == 0) {
|
||||
return StreamSvgIcon.check(
|
||||
size: style.fontSize,
|
||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
'Uploading $uploadRemaining/$totalAttachments ...',
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
Widget child = SendingIndicator(
|
||||
message: widget.message,
|
||||
message: message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style.fontSize,
|
||||
);
|
||||
@@ -1032,18 +1018,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
if (widget.message.status == MessageSendingStatus.failed_update) {
|
||||
StreamChat.of(context).client.updateMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.updateMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.deleteMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
@@ -200,10 +199,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
@@ -231,8 +227,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||
child: child,
|
||||
),
|
||||
@@ -294,10 +290,9 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
},
|
||||
imageUrl:
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -901,4 +901,16 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.retry({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'icon_retry.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class UploadProgressIndicator extends StatelessWidget {
|
||||
final int uploaded;
|
||||
final int total;
|
||||
final Color progressIndicatorColor;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final bool showBackground;
|
||||
final TextStyle textStyle;
|
||||
|
||||
const UploadProgressIndicator({
|
||||
Key key,
|
||||
@required this.uploaded,
|
||||
@required this.total,
|
||||
this.progressIndicatorColor = const Color(0xffb2b2b2),
|
||||
this.padding = const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||
this.showBackground = true,
|
||||
this.textStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget child = Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${fileSize(uploaded, 1)}/${fileSize(total, 1)}',
|
||||
style: textStyle ??
|
||||
theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (showBackground) {
|
||||
child = Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'dart:math';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
@@ -215,7 +216,7 @@ String getWebsiteName(String hostName) {
|
||||
}
|
||||
|
||||
/// A method returns a human readable string representing a file _size
|
||||
String filesize(dynamic size, [int round = 2]) {
|
||||
String fileSize(dynamic size, [int round = 2]) {
|
||||
if (size == null) return 'Size N/A';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
|
||||
class VideoAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final Message message;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VideoPackage videoPackage;
|
||||
|
||||
VideoAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.messageTheme,
|
||||
this.videoPackage,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentState createState() => _VideoAttachmentState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentState extends State<VideoAttachment> {
|
||||
bool initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.videoPackage.onInit = () {
|
||||
setState(() {
|
||||
initialized = true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.videoPackage.initialised) {
|
||||
return Container(
|
||||
height: widget.size?.height ?? 100,
|
||||
width: widget.size?.width ?? 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
var res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [widget.attachment],
|
||||
userName: widget.message.user.name,
|
||||
sentAt: widget.message.createdAt,
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (res != null) {
|
||||
widget.onReturnAction(res);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: widget.size?.height,
|
||||
width: widget.size?.width,
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Chewie(
|
||||
controller: widget.videoPackage.chewieController,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.attachment.title != null)
|
||||
Material(
|
||||
color: widget.messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: widget.messageTheme,
|
||||
attachment: widget.attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class IVideoService {
|
||||
static final IVideoService instance = IVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
IVideoService._();
|
||||
|
||||
/// compress video from [path]
|
||||
/// compress video from [path] return [Future<MediaInfo>]
|
||||
///
|
||||
/// you can choose its quality by [quality],
|
||||
/// determine whether to delete his source file by [deleteOrigin]
|
||||
/// optional parameters [startTime] [duration] [includeAudio] [frameRate]
|
||||
///
|
||||
/// ## example
|
||||
/// ```dart
|
||||
/// final info = await _flutterVideoCompress.compressVideo(
|
||||
/// file.path,
|
||||
/// deleteOrigin: true,
|
||||
/// );
|
||||
/// debugPrint(info.toJson());
|
||||
/// ```
|
||||
Future<MediaInfo> compressVideo(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...).
|
||||
/// The video can be a local video file, or an URL repreents iOS or Android native supported video format.
|
||||
/// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video.
|
||||
/// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format.
|
||||
Future<Uint8List> generateVideoThumbnail({
|
||||
@required String video,
|
||||
ImageFormat imageFormat = ImageFormat.PNG,
|
||||
int maxHeight = 0,
|
||||
int maxWidth = 0,
|
||||
int timeMs = 0,
|
||||
int quality = 10,
|
||||
}) {
|
||||
return VideoThumbnail.thumbnailData(
|
||||
video: video,
|
||||
imageFormat: imageFormat,
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
timeMs: timeMs,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
IVideoService get VideoService => IVideoService.instance;
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'video_service.dart';
|
||||
|
||||
class VideoThumbnailImage extends StatefulWidget {
|
||||
final String video;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
final ImageFormat format;
|
||||
final Widget Function(BuildContext, Object) errorBuilder;
|
||||
final WidgetBuilder placeholderBuilder;
|
||||
|
||||
const VideoThumbnailImage({
|
||||
Key key,
|
||||
@required this.video,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.format = ImageFormat.PNG,
|
||||
this.errorBuilder,
|
||||
this.placeholderBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoThumbnailImageState createState() => _VideoThumbnailImageState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
||||
Future<Uint8List> thumbnailFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VideoThumbnailImage oldWidget) {
|
||||
if (oldWidget.video != widget.video || oldWidget.format != widget.format) {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: thumbnailFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(context, snapshot.error);
|
||||
}
|
||||
return Center(child: StreamSvgIcon.error());
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
if (widget.placeholderBuilder != null) {
|
||||
return widget.placeholderBuilder(context);
|
||||
}
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
final data = snapshot.data;
|
||||
return Image.memory(
|
||||
data,
|
||||
fit: widget.fit,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user