feat: more implementation.

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2023-07-28 21:07:02 +05:30
committed by xsahil03x
parent f246de55a1
commit e8de28854b
93 changed files with 883 additions and 843 deletions
@@ -499,7 +499,7 @@ class Channel {
]);
}
final isImage = it.type == 'image';
final isImage = it.type == AttachmentType.image;
final cancelToken = CancelToken();
Future<SendAttachmentResponse> future;
if (isImage) {
@@ -16,6 +16,7 @@ mixin AttachmentType {
static const file = 'file';
static const giphy = 'giphy';
static const video = 'video';
static const audio = 'audio';
/// Application custom types.
static const urlPreview = 'url_preview';
@@ -53,19 +54,15 @@ class Attachment extends Equatable {
}) : id = id ?? const Uuid().v4(),
_type = type,
title = title ?? file?.name,
_uploadState = uploadState,
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
// For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData].
extraData = {
...extraData,
if (file?.size != null) 'file_size': file?.size,
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
} {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType,
};
/// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) =>
@@ -82,7 +79,8 @@ class Attachment extends Equatable {
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
Attachment(
type: ogAttachment.type,
// If the type is not specified, we default to urlPreview.
type: ogAttachment.type ?? AttachmentType.urlPreview,
title: ogAttachment.title,
titleLink: ogAttachment.titleLink,
text: ogAttachment.text,
@@ -98,7 +96,9 @@ class Attachment extends Equatable {
///The attachment type based on the URL resource. This can be: audio,
///image or video
String? get type {
if (_type == AttachmentType.image && titleLink != null) {
// If the attachment contains titleLink but is not of type giphy, we
// consider it as a urlPreview.
if (_type != AttachmentType.giphy && titleLink != null) {
return AttachmentType.urlPreview;
}
@@ -107,6 +107,9 @@ class Attachment extends Equatable {
final String? _type;
/// The raw attachment type.
String? get rawType => _type;
///The link to which the attachment message points to.
final String? titleLink;
@@ -159,7 +162,15 @@ class Attachment extends Equatable {
final AttachmentFile? file;
/// The current upload state of the attachment
late final UploadState uploadState;
UploadState get uploadState {
if (_uploadState case final state?) return state;
return ((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
final UploadState? _uploadState;
/// Map of custom channel extraData
final Map<String, Object?> extraData;
@@ -62,7 +62,7 @@ class AttachmentFile {
String? get extension => name?.split('.').last;
/// The mime type of this file.
MediaType? get mimeType => name?.mimeType;
MediaType? get mediaType => name?.mediaType;
/// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
@@ -75,13 +75,13 @@ class AttachmentFile {
multiPartFile = MultipartFile.fromBytes(
bytes!,
filename: name,
contentType: mimeType,
contentType: mediaType,
);
} else {
multiPartFile = await MultipartFile.fromFile(
path!,
filename: name,
contentType: mimeType,
contentType: mediaType,
);
}
return multiPartFile;
@@ -20,8 +20,8 @@ extension MapX<K, V> on Map<K?, V?> {
/// Useful extension functions for [String]
extension StringX on String {
/// returns the mime type from the passed file name.
MediaType? get mimeType {
/// returns the media type from the passed file name.
MediaType? get mediaType {
if (toLowerCase().endsWith('heic')) {
return MediaType.parse('image/heic');
} else {
@@ -25,13 +25,13 @@ void main() {
group('mimeType', () {
test('should return null if `String` is not a filename', () {
const fileName = 'not-a-file-name';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNull);
});
test('should return mimeType if string is a filename', () {
const fileName = 'dummyFileName.jpeg';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'jpeg');
@@ -39,7 +39,7 @@ void main() {
test('should return `image/heic` if ends with `heic`', () {
const fileName = 'dummyFileName.heic';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'heic');
@@ -57,6 +57,8 @@ class AttachmentWidgetCatalog {
extension on List<Attachment> {
/// Groups the attachments by their type.
Map<String, List<Attachment>> get grouped {
return groupBy(this, (attachment) => attachment.type!);
return groupBy(where((it) {
return it.type != null;
}), (attachment) => attachment.type!);
}
}
@@ -1,20 +1,13 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/file_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
import 'package:stream_chat_flutter/src/attachment/gallery_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/giphy_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/image_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/url_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/video_attachment.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../attachment_upload_state_builder.dart';
part 'fallback_attachment_builder.dart';
part 'file_attachment_builder.dart';
@@ -82,41 +75,58 @@ abstract class StreamAttachmentWidgetBuilder {
/// widget.
static List<StreamAttachmentWidgetBuilder> defaultBuilders({
required Message message,
ShapeBorder? shape,
EdgeInsetsGeometry padding = const EdgeInsets.all(4),
StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) {
return [
// Handles a mix of image, gif, video, and file attachments.
// Handles a mix of image, gif, video, url and file attachments.
MixedAttachmentBuilder(
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles a mix of image, gif, and video attachments.
GalleryAttachmentBuilder(
shape: shape,
padding: padding,
runSpacing: padding.vertical / 2,
spacing: padding.horizontal / 2,
onAttachmentTap: onAttachmentTap,
),
// Handles file attachments.
FileAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles giphy attachments.
GiphyAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles image attachments.
ImageAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles video attachments.
VideoAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// We don't handle URL attachments if the message is a reply.
if (message.quotedMessage == null)
UrlAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
@@ -93,16 +93,6 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
attachments: galleryAttachments,
itemBuilder: (context, index) {
final attachment = galleryAttachments[index];
final attachmentType = attachment.type;
final isImage = attachmentType == AttachmentType.image;
final isVideo = attachmentType == AttachmentType.video;
final isGiphy = attachmentType == AttachmentType.giphy;
assert(
isImage || isVideo || isGiphy,
'Attachment type should be image, video or giphy',
);
VoidCallback? onTap;
if (onAttachmentTap != null) {
@@ -112,29 +102,13 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
return InkWell(
onTap: onTap,
child: Stack(
alignment: Alignment.center,
children: [
if (isImage)
StreamImageAttachmentThumbnail(
image: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
)
else if (isVideo)
StreamVideoAttachmentThumbnail(
video: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
)
else if (isGiphy)
StreamGiphyAttachmentThumbnail(
giphy: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
StreamMediaAttachmentThumbnail(
media: attachment,
width: constraints.maxWidth,
height: constraints.maxHeight,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
@@ -62,6 +62,7 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: InkWell(
onTap: onTap,
child: StreamImageAttachment(
shape: shape,
message: message,
constraints: constraints,
image: image,
@@ -3,67 +3,71 @@ part of 'attachment_widget_builder.dart';
/// {@template mixedAttachmentBuilder}
/// A widget builder for Mixed attachment type.
///
/// This builder is used when a message contains both image/video/giphy and file
/// attachments.
/// This builder is used when a message contains a mix of media type and file
/// or url preview attachments.
///
/// This builder will render first image/video/giphy attachment and then render
/// the file attachments.
/// This builder will render first the url preview or file attachment and then
/// the media attachments.
/// {@endtemplate}
class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
/// {@macro mixedAttachmentBuilder}
MixedAttachmentBuilder({
this.shape,
this.padding = const EdgeInsets.all(4),
this.onAttachmentTap,
StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) : _imageAttachmentBuilder = ImageAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_videoAttachmentBuilder = VideoAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_giphyAttachmentBuilder = GiphyAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_galleryAttachmentBuilder = GalleryAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_fileAttachmentBuilder = FileAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
),
_urlAttachmentBuilder = UrlAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
);
/// The shape of the gallery attachment.
final ShapeBorder? shape;
/// The padding to apply to the gallery attachment widget.
/// The padding to apply to the mixed attachment widget.
final EdgeInsetsGeometry padding;
/// The callback to call when the attachment is tapped.
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _urlAttachmentBuilder;
@override
bool canHandle(
Message message,
Map<String, List<Attachment>> attachments,
) {
final containsImage = attachments.keys.contains(AttachmentType.image);
final containsVideo = attachments.keys.contains(AttachmentType.video);
final containsGiphy = attachments.keys.contains(AttachmentType.giphy);
final containsFile = attachments.keys.contains(AttachmentType.file);
final types = attachments.keys;
final containsImage = types.contains(AttachmentType.image);
final containsVideo = types.contains(AttachmentType.video);
final containsGiphy = types.contains(AttachmentType.giphy);
final containsFile = types.contains(AttachmentType.file);
final containsUrlPreview = types.contains(AttachmentType.urlPreview);
final containsMedia = containsImage || containsVideo || containsGiphy;
return containsMedia && containsFile;
return containsMedia && containsFile ||
containsMedia && containsUrlPreview ||
containsFile && containsUrlPreview ||
containsMedia && containsFile && containsUrlPreview;
}
@override
@@ -74,6 +78,7 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
) {
assert(debugAssertCanHandle(message, attachments), '');
final urls = attachments[AttachmentType.urlPreview];
final files = attachments[AttachmentType.file];
final images = attachments[AttachmentType.image];
final videos = attachments[AttachmentType.video];
@@ -86,11 +91,14 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (urls != null)
_urlAttachmentBuilder.build(context, message, {
AttachmentType.urlPreview: urls,
}),
if (files != null)
for (final file in files)
_fileAttachmentBuilder.build(context, message, {
AttachmentType.file: [file],
}),
_fileAttachmentBuilder.build(context, message, {
AttachmentType.file: files,
}),
if (shouldBuildGallery)
_galleryAttachmentBuilder.build(context, message, {
if (images != null) AttachmentType.image: images,
@@ -128,19 +128,21 @@ class _FileTypeImage extends StatelessWidget {
file: file,
width: double.infinity,
height: double.infinity,
// fit: BoxFit.cover,
);
final mimeType = file.title?.mimeType?.type;
final isImage = mimeType == 'image';
final isVideo = mimeType == 'video';
final mediaType = file.title?.mediaType;
final isImage = mediaType?.type == AttachmentType.image;
final isVideo = mediaType?.type == AttachmentType.video;
if (isImage || isVideo) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
child = Container(
clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(color: colorTheme.borders),
side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(8),
),
),
@@ -127,6 +127,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1],
[1],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -145,6 +147,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: const [
[1, 1],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -170,6 +174,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: [
if (isLandscape1) [2, 1] else [1, 2],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -204,6 +210,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1],
[1, 1],
],
spacing: spacing,
runSpacing: runSpacing,
reverse: !isLandscape1,
children: [
itemBuilder(context, 0),
@@ -238,6 +246,8 @@ class StreamGalleryAttachment extends StatelessWidget {
return FlexGrid(
pattern: pattern,
maxChildren: 4,
spacing: spacing,
runSpacing: runSpacing,
children: children,
overlayBuilder: (context, remaining) {
return IgnorePointer(
@@ -12,7 +12,7 @@ class StreamGiphyAttachment extends StatelessWidget {
super.key,
required this.message,
required this.giphy,
this.type = GiphyInfoType.fixedHeightDownsampled,
this.type = GiphyInfoType.original,
this.shape,
this.constraints = const BoxConstraints(),
});
@@ -50,18 +50,18 @@ Future<AttachmentData> downloadAttachmentData(
String? downloadUrl;
String? fileName;
/* ---IMAGES/GIFS--- */
if (type == 'image') {
if (type == AttachmentType.image) {
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
fileName = attachment.title;
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
}
/* ---GIPHY's--- */
else if (type == 'giphy') {
else if (type == AttachmentType.giphy) {
downloadUrl = attachment.thumbUrl;
fileName = '${attachment.title}.gif';
}
/* ---FILES AND VIDEOS--- */
else if (type == 'file' || type == 'video') {
else if (type == AttachmentType.file || type == AttachmentType.video) {
downloadUrl = attachment.assetUrl;
fileName = attachment.title;
}
@@ -50,9 +50,9 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mimeType = file.title?.mimeType?.type;
final mediaType = file.title?.mediaType;
final isImage = mimeType == 'image';
final isImage = mediaType?.type == AttachmentType.image;
if (isImage) {
return StreamImageAttachmentThumbnail(
image: file,
@@ -62,7 +62,7 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
);
}
final isVideo = mimeType == 'video';
final isVideo = mediaType?.type == AttachmentType.video;
if (isVideo) {
return StreamVideoAttachmentThumbnail(
video: file,
@@ -73,6 +73,6 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
}
// Return a generic file type icon.
return getFileTypeImage(mimeType);
return getFileTypeImage(mediaType?.mimeType);
}
}
@@ -50,6 +50,9 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -2,49 +2,12 @@ import 'dart:io' show File;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:image_size_getter/file_input.dart'; // For compatibility with flutter web.
import 'package:image_size_getter/image_size_getter.dart' hide Size;
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
extension AspectRatioX on Attachment {
/// Returns the size of the attachment if it is an image or giffy.
/// Otherwise, returns null.
Size? get originalSize {
// Return null if the attachment is not an image or giffy.
if (type != 'image' && type != 'giphy') return null;
// Calculate size locally if the attachment is not uploaded yet.
final file = this.file;
if (file != null) {
ImageInput? input;
if (file.bytes != null) {
input = MemoryInput(file.bytes!);
} else if (file.path != null) {
input = FileInput(File(file.path!));
}
// Return null if the file does not contain enough information.
if (input == null) return null;
final size = ImageSizeGetter.getSize(input);
if (size.needRotate) {
return Size(size.height.toDouble(), size.width.toDouble());
}
return Size(size.width.toDouble(), size.height.toDouble());
}
// Otherwise, use the size provided by the server.
final width = originalWidth;
final height = originalHeight;
if (width == null || height == null) return null;
return Size(width.toDouble(), height.toDouble());
}
}
/// {@template imageAttachmentThumbnail}
/// Widget for building image attachment thumbnail.
///
@@ -101,6 +64,9 @@ class StreamImageAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@template mediaAttachmentThumbnail}
/// Widget for building media attachment thumbnail.
///
/// This widget is used when the [Attachment.type] is [AttachmentType.image],
/// [AttachmentType.video] or [AttachmentType.giphy].
///
/// see also:
/// * [StreamImageAttachmentThumbnail]
/// * [StreamVideoAttachmentThumbnail]
/// * [StreamGiphyAttachmentThumbnail]
/// {@endtemplate}
class StreamMediaAttachmentThumbnail extends StatelessWidget {
/// {@macro mediaAttachmentThumbnail}
const StreamMediaAttachmentThumbnail({
super.key,
required this.media,
this.width,
this.height,
this.fit,
this.thumbnailSize,
this.thumbnailResizeType = 'clip',
this.thumbnailCropType = 'center',
this.gifInfoType = GiphyInfoType.original,
this.errorBuilder = _defaultErrorBuilder,
});
/// The giphy attachment to build the thumbnail for.
final Attachment media;
/// The width of the thumbnail.
final double? width;
/// The height of the thumbnail.
final double? height;
/// How to inscribe the thumbnail into the space allocated during layout.
final BoxFit? fit;
/// Builder used when the thumbnail fails to load.
final ThumbnailErrorBuilder errorBuilder;
/// Size of the attachment image thumbnail.
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final Size? thumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final String /*clip|crop|scale|fill*/ thumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final String /*center|top|bottom|left|right*/ thumbnailCropType;
/// The type of giphy thumbnail to build.
///
/// Ignored if the [Attachment.type] is not [AttachmentType.giphy].
final GiphyInfoType gifInfoType;
// Default error builder for image attachment thumbnail.
static Widget _defaultErrorBuilder(
BuildContext context,
Object error,
StackTrace? stackTrace,
) {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@override
Widget build(BuildContext context) {
final type = media.type;
if (type == AttachmentType.image) {
return StreamImageAttachmentThumbnail(
image: media,
width: width,
height: height,
fit: fit,
thumbnailSize: thumbnailSize,
thumbnailResizeType: thumbnailResizeType,
thumbnailCropType: thumbnailCropType,
errorBuilder: errorBuilder,
);
}
if (type == AttachmentType.giphy) {
return StreamGiphyAttachmentThumbnail(
giphy: media,
width: width,
height: height,
fit: fit,
type: gifInfoType,
errorBuilder: errorBuilder,
);
}
if (type == AttachmentType.video) {
return StreamVideoAttachmentThumbnail(
video: media,
width: width,
height: height,
fit: fit,
errorBuilder: errorBuilder,
);
}
return errorBuilder(
context,
'Unsupported attachment type: $type',
StackTrace.current,
);
}
}
@@ -21,8 +21,20 @@ class ThumbnailError extends StatelessWidget {
super.key,
required this.error,
this.stackTrace,
this.width,
this.height,
this.fit,
});
/// The width of the thumbnail.
final double? width;
/// The height of the thumbnail.
final double? height;
/// How to inscribe the thumbnail into the space allocated during layout.
final BoxFit? fit;
/// The error that triggered this error widget.
final Object error;
@@ -33,7 +45,9 @@ class ThumbnailError extends StatelessWidget {
Widget build(BuildContext context) {
return Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
width: width,
height: height,
fit: fit,
package: 'stream_chat_flutter',
);
}
@@ -46,6 +46,9 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -47,7 +47,7 @@ class StreamUrlAttachment extends StatelessWidget {
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(8),
);
final backgroundColor = messageTheme.urlAttachmentBackgroundColor;
@@ -62,44 +62,43 @@ class StreamUrlAttachment extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (urlAttachment.imageUrl != null)
Stack(
children: [
AspectRatio(
// Default aspect ratio for Open Graph images.
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
aspectRatio: 1.91 / 1,
child: StreamImageAttachmentThumbnail(
image: urlAttachment,
fit: BoxFit.cover,
),
Stack(
children: [
AspectRatio(
// Default aspect ratio for Open Graph images.
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
aspectRatio: 1.91 / 1,
child: StreamImageAttachmentThumbnail(
image: urlAttachment,
fit: BoxFit.cover,
),
Positioned(
left: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
color: backgroundColor,
),
Positioned(
left: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 12,
bottom: 4,
),
child: Text(
hostDisplayName,
style: messageTheme.urlAttachmentHostStyle,
),
color: backgroundColor,
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 12,
bottom: 4,
),
child: Text(
hostDisplayName,
style: messageTheme.urlAttachmentHostStyle,
),
),
),
],
),
),
],
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
@@ -129,7 +129,7 @@ class AttachmentActionsModal extends StatelessWidget {
if (showSave)
_buildButton(
context,
attachment.type == 'video'
attachment.type == AttachmentType.video
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
@@ -36,11 +36,11 @@ class StreamMessagePreviewText extends StatelessWidget {
final messageTextParts = [
...messageAttachments.map((it) {
if (it.type == 'image') {
if (it.type == AttachmentType.image) {
return '📷';
} else if (it.type == 'video') {
} else if (it.type == AttachmentType.video) {
return '🎬';
} else if (it.type == 'giphy') {
} else if (it.type == AttachmentType.giphy) {
return '[GIF]';
}
return it == message.attachments.last
@@ -7,7 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart';
import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -272,7 +272,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
}
}
if (widget.autoplayVideos &&
currentAttachment.type == 'video') {
currentAttachment.type == AttachmentType.video) {
final controller = videoPackages[currentAttachment.id]!;
controller._chewieController?.play();
}
@@ -294,47 +294,19 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
child: ContextMenuArea(
verticalPadding: 0,
builder: (_) => [
DownloadMenuItem(
attachment: attachment,
),
DownloadMenuItem(attachment: attachment),
],
child: PhotoView.customChild(
heroAttributes: PhotoViewHeroAttributes(
tag: attachment.id,
),
// imageProvider: (imageUrl == null &&
// attachment.localUri != null &&
// attachment.file?.bytes != null)
// ? Image.memory(attachment.file!.bytes!).image
// : CachedNetworkImageProvider(imageUrl!),
// errorBuilder: (_, __, ___) => const AttachmentError(),
// loadingBuilder: (context, _) {
// final image = Image.asset(
// 'images/placeholder.png',
// fit: BoxFit.cover,
// package: 'stream_chat_flutter',
// );
// final colorTheme =
// StreamChatTheme.of(context).colorTheme;
// return Shimmer.fromColors(
// baseColor: colorTheme.disabled,
// highlightColor: colorTheme.inputBg,
// child: image,
// );
// },
child: StreamImageAttachmentThumbnail(
image: attachment,
width: double.infinity,
height: double.infinity,
),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
// heroAttributes: PhotoViewHeroAttributes(
// tag: widget.mediaAttachmentPackages,
// ),
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
child: StreamMediaAttachmentThumbnail(
media: attachment,
width: double.infinity,
height: double.infinity,
),
),
),
),
@@ -353,9 +325,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
child: ContextMenuArea(
verticalPadding: 0,
builder: (_) => [
DownloadMenuItem(
attachment: attachment,
),
DownloadMenuItem(attachment: attachment),
],
child: Chewie(
controller: controller.chewieController!,
@@ -94,7 +94,7 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
_pageController = PageController(initialPage: widget.startIndex);
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
final attachment = widget.mediaAttachmentPackages[i].attachment;
if (attachment.type != 'video') continue;
if (attachment.type != AttachmentType.video) continue;
final package = DesktopVideoPackage(attachment);
videoPackages[attachment.id] = package;
}
@@ -298,7 +298,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
p.player.pause();
}
}
if (widget.autoplayVideos && currentAttachment.type == 'video') {
if (widget.autoplayVideos &&
currentAttachment.type == AttachmentType.video) {
final package = videoPackages[currentAttachment.id]!;
package.player.play();
}
@@ -307,7 +308,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
final currentAttachmentPackage =
widget.mediaAttachmentPackages[index];
final attachment = currentAttachmentPackage.attachment;
if (attachment.type == 'image' || attachment.type == 'giphy') {
if (attachment.type == AttachmentType.image ||
attachment.type == AttachmentType.giphy) {
final imageUrl = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
@@ -359,7 +361,7 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
),
),
);
} else if (attachment.type == 'video') {
} else if (attachment.type == AttachmentType.video) {
final package = videoPackages[attachment.id]!;
package.player.open(
Playlist(
@@ -95,7 +95,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
final url = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl!;
final type = attachment.type == 'image'
final type = attachment.type == AttachmentType.image
? 'jpg'
: url.split('?').first.split('.').last;
final request = await HttpClient().getUrl(Uri.parse(url));
@@ -218,7 +218,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
widget.mediaAttachmentPackages[index];
final attachment = attachmentPackage.attachment;
final message = attachmentPackage.message;
if (attachment.type == 'video') {
if (attachment.type == AttachmentType.video) {
media = MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
@@ -114,119 +114,121 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final child = Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (widget.showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
widget.message,
constraints,
fontSize,
orientation,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (widget.showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
widget.message,
constraints,
fontSize,
orientation,
),
0,
),
0,
),
child: StreamReactionPicker(
message: widget.message,
),
);
},
child: StreamReactionPicker(
message: widget.message,
),
);
},
),
const SizedBox(height: 10),
IgnorePointer(
child: widget.messageWidget,
),
const SizedBox(height: 10),
IgnorePointer(
child: widget.messageWidget,
),
const SizedBox(height: 8),
Padding(
padding: EdgeInsets.only(
left: widget.reverse ? 0 : 40,
),
child: SizedBox(
width: mediaQueryData.size.width * 0.75,
child: Material(
color: streamChatThemeData.colorTheme.appBg,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.showReplyMessage &&
widget.message.state.isCompleted)
ReplyButton(
onTap: () {
Navigator.of(context).pop();
if (widget.onReplyTap != null) {
widget.onReplyTap?.call(widget.message);
}
},
const SizedBox(height: 8),
Padding(
padding: EdgeInsets.only(
left: widget.reverse ? 0 : 40,
),
child: SizedBox(
width: mediaQueryData.size.width * 0.75,
child: Material(
color: streamChatThemeData.colorTheme.appBg,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.showReplyMessage &&
widget.message.state.isCompleted)
ReplyButton(
onTap: () {
Navigator.of(context).pop();
if (widget.onReplyTap != null) {
widget.onReplyTap?.call(widget.message);
}
},
),
if (widget.showThreadReplyMessage &&
(widget.message.state.isCompleted) &&
widget.message.parentId == null)
ThreadReplyButton(
message: widget.message,
onThreadReplyTap: widget.onThreadReplyTap,
),
if (widget.showResendMessage)
ResendMessageButton(
message: widget.message,
channel: channel,
),
if (widget.showEditMessage)
EditMessageButton(
onTap: () {
Navigator.of(context).pop();
_showEditBottomSheet(context);
},
),
if (widget.showCopyMessage)
CopyMessageButton(
onTap: () {
widget.onCopyTap?.call(widget.message);
Navigator.of(context).pop();
},
),
if (widget.showFlagButton)
FlagMessageButton(
onTap: _showFlagDialog,
),
if (widget.showPinButton)
PinMessageButton(
onTap: _togglePin,
pinned: widget.message.pinned,
),
if (widget.showDeleteMessage)
DeleteMessageButton(
isDeleteFailed:
widget.message.state.isDeletingFailed,
onTap: _showDeleteBottomSheet,
),
...widget.customActions
.map((action) => _buildCustomAction(
context,
action,
)),
].insertBetween(
Container(
height: 1,
color: streamChatThemeData.colorTheme.borders,
),
if (widget.showThreadReplyMessage &&
(widget.message.state.isCompleted) &&
widget.message.parentId == null)
ThreadReplyButton(
message: widget.message,
onThreadReplyTap: widget.onThreadReplyTap,
),
if (widget.showResendMessage)
ResendMessageButton(
message: widget.message,
channel: channel,
),
if (widget.showEditMessage)
EditMessageButton(
onTap: () {
Navigator.of(context).pop();
_showEditBottomSheet(context);
},
),
if (widget.showCopyMessage)
CopyMessageButton(
onTap: () {
widget.onCopyTap?.call(widget.message);
Navigator.of(context).pop();
},
),
if (widget.showFlagButton)
FlagMessageButton(
onTap: _showFlagDialog,
),
if (widget.showPinButton)
PinMessageButton(
onTap: _togglePin,
pinned: widget.message.pinned,
),
if (widget.showDeleteMessage)
DeleteMessageButton(
isDeleteFailed:
widget.message.state.isDeletingFailed,
onTap: _showDeleteBottomSheet,
),
...widget.customActions
.map((action) => _buildCustomAction(
context,
action,
)),
].insertBetween(
Container(
height: 1,
color: streamChatThemeData.colorTheme.borders,
),
),
),
),
),
),
],
],
),
),
),
),
@@ -217,7 +217,7 @@ extension StreamImagePickerX on StreamAttachmentPickerController {
final extraDataMap = <String, Object>{};
final mimeType = file.mimeType?.mimeType;
final mimeType = file.mediaType?.mimeType;
if (mimeType != null) {
extraDataMap['mime_type'] = mimeType;
@@ -240,10 +240,10 @@ class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption {
extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController {
/// Returns the list of available attachment picker options.
Set<AttachmentPickerType> get currentAttachmentPickerTypes {
final containsImage = value.any((it) => it.type == 'image');
final containsVideo = value.any((it) => it.type == 'video');
final containsAudio = value.any((it) => it.type == 'audio');
final containsFile = value.any((it) => it.type == 'file');
final containsImage = value.any((it) => it.type == AttachmentType.image);
final containsVideo = value.any((it) => it.type == AttachmentType.video);
final containsAudio = value.any((it) => it.type == AttachmentType.audio);
final containsFile = value.any((it) => it.type == AttachmentType.file);
return {
if (containsImage) AttachmentPickerType.images,
@@ -1,10 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/file_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
typedef _Builders = Map<String, QuotedMessageAttachmentThumbnailBuilder>;
/// {@template streamQuotedMessage}
/// Widget for the quoted message.
@@ -40,8 +41,7 @@ class StreamQuotedMessageWidget extends StatelessWidget {
final int textLimit;
/// Map that defines a thumbnail builder for an attachment type
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
attachmentThumbnailBuilders;
final _Builders? attachmentThumbnailBuilders;
/// Padding around the widget
final EdgeInsetsGeometry padding;
@@ -109,19 +109,17 @@ class _QuotedMessage extends StatelessWidget {
final bool reverse;
final Widget Function(BuildContext, Message)? textBuilder;
/// Map that defines a thumbnail builder for an attachment type
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
attachmentThumbnailBuilders;
final _Builders? attachmentThumbnailBuilders;
bool get _hasAttachments => message.attachments.isNotEmpty;
bool get _containsText => message.text?.isNotEmpty == true;
bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null);
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
bool get _isGiphy =>
message.attachments.any((element) => element.type == 'giphy');
bool get _isGiphy => message.attachments
.any((element) => element.type == AttachmentType.giphy);
bool get _isDeleted => message.isDeleted || message.deletedAt != null;
@@ -150,14 +148,6 @@ class _QuotedMessage extends StatelessWidget {
} else {
// Show quoted message
children = [
if (onQuotedMessageClear != null)
PlatformWidgetBuilder(
web: (context, child) => child,
desktop: (context, child) => child,
child: ClearInputItemButton(
onTap: onQuotedMessageClear,
),
),
if (_hasAttachments)
_ParseAttachments(
message: message,
@@ -184,9 +174,26 @@ class _QuotedMessage extends StatelessWidget {
),
),
),
].insertBetween(const SizedBox(width: 8));
];
}
// Add clear button if needed.
if (onQuotedMessageClear != null) {
children.insert(
0,
PlatformWidgetBuilder(
web: (context, child) => child,
desktop: (context, child) => child,
child: ClearInputItemButton(
onTap: onQuotedMessageClear,
),
),
);
}
// Add some spacing between the children.
children = children.insertBetween(const SizedBox(width: 8));
return Container(
decoration: BoxDecoration(
color: _getBackgroundColor(context),
@@ -229,193 +236,106 @@ class _ParseAttachments extends StatelessWidget {
final Message message;
final StreamMessageThemeData messageTheme;
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
attachmentThumbnailBuilders;
bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null);
final _Builders? attachmentThumbnailBuilders;
@override
Widget build(BuildContext context) {
Widget child;
Attachment attachment;
if (_containsLinkAttachment) {
attachment = message.attachments.firstWhere(
(element) => element.ogScrapeUrl != null || element.titleLink != null,
final attachment = message.attachments.first;
var attachmentBuilders = attachmentThumbnailBuilders;
attachmentBuilders ??= _createDefaultAttachmentBuilders();
// Build the attachment widget using the builder for the attachment type.
final attachmentWidget = attachmentBuilders[attachment.type]?.call(
context,
attachment,
);
// Return empty container if no attachment widget is returned.
if (attachmentWidget == null) return const SizedBox.shrink();
final colorTheme = StreamChatTheme.of(context).colorTheme;
var clipBehavior = Clip.none;
ShapeDecoration? decoration;
if (attachment.type != AttachmentType.file) {
clipBehavior = Clip.hardEdge;
decoration = ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(8),
),
);
child = _UrlAttachment(attachment: attachment);
} else {
QuotedMessageAttachmentThumbnailBuilder? attachmentBuilder;
attachment = message.attachments.last;
if (attachmentThumbnailBuilders?.containsKey(attachment.type) == true) {
attachmentBuilder = attachmentThumbnailBuilders![attachment.type];
}
attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
if (attachmentBuilder == null) {
child = const Offstage();
} else {
child = attachmentBuilder(context, attachment);
}
}
final isImageFile = attachment.title?.mimeType?.type == 'image';
final isVideoFile = attachment.title?.mimeType?.type == 'video';
return Container(
key: Key(attachment.id),
clipBehavior: clipBehavior,
decoration: decoration,
constraints: const BoxConstraints.tightFor(width: 36, height: 36),
child: AbsorbPointer(child: attachmentWidget),
);
}
return Material(
clipBehavior: Clip.hardEdge,
type: MaterialType.transparency,
shape: attachment.type == 'file' && (!isImageFile && !isVideoFile)
? null
: RoundedRectangleBorder(
side: const BorderSide(width: 0, color: Colors.transparent),
_Builders _createDefaultAttachmentBuilders() {
Widget _createMediaThumbnail(BuildContext context, Attachment media) {
return StreamImageAttachmentThumbnail(
image: media,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
}
Widget _createUrlThumbnail(BuildContext context, Attachment media) {
return StreamImageAttachmentThumbnail(
image: media,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
}
Widget _createFileThumbnail(BuildContext context, Attachment file) {
Widget thumbnail = StreamFileAttachmentThumbnail(
file: file,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
final mediaType = file.title?.mediaType;
final isImage = mediaType?.type == AttachmentType.image;
final isVideo = mediaType?.type == AttachmentType.video;
if (isImage || isVideo) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
thumbnail = Container(
clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(8),
),
child: AbsorbPointer(child: child),
);
}
Map<String, QuotedMessageAttachmentThumbnailBuilder>
get _defaultAttachmentBuilder {
final builders = <String, QuotedMessageAttachmentThumbnailBuilder>{
'image': (_, attachment) {
return StreamImageAttachment(
message: message,
image: attachment,
constraints: BoxConstraints.loose(const Size(32, 32)),
);
},
'video': (_, attachment) {
return StreamVideoAttachmentThumbnail(
key: ValueKey(attachment.assetUrl),
video: attachment,
width: 32,
height: 32,
// constraints: BoxConstraints.loose(const Size(32, 32)),
// errorBuilder: (_, __) => AttachmentError(
// constraints: BoxConstraints.loose(const Size(32, 32)),
// ),
);
},
'giphy': (_, attachment) {
const size = Size(32, 32);
return CachedNetworkImage(
height: size.height,
width: size.width,
placeholder: (_, __) {
return SizedBox(
width: size.width,
height: size.height,
child: const Center(
child: CircularProgressIndicator.adaptive(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl!,
errorWidget: (context, url, error) =>
AttachmentError(constraints: BoxConstraints.loose(size)),
fit: BoxFit.cover,
);
},
};
builders['file'] = (_, attachment) {
return SizedBox(
height: 32,
width: 32,
child: Builder(
builder: (context) {
final isImageFile = attachment.title?.mimeType?.type == 'image';
if (isImageFile) {
return builders['image']!(context, attachment);
}
final isVideoFile = attachment.title?.mimeType?.type == 'video';
if (isVideoFile) {
return builders['video']!(context, attachment);
}
return getFileTypeImage(
attachment.extraData['mime_type'] as String?,
);
},
),
);
};
return builders;
}
}
class _UrlAttachment extends StatelessWidget {
const _UrlAttachment({
required this.attachment,
});
final Attachment attachment;
@override
Widget build(BuildContext context) {
const size = Size(32, 32);
if (attachment.thumbUrl != null) {
return Container(
height: size.height,
width: size.width,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
attachment.thumbUrl!,
),
),
),
);
child: thumbnail,
);
}
return thumbnail;
}
return AttachmentError(constraints: BoxConstraints.loose(size));
}
}
class _VideoAttachmentThumbnail extends StatefulWidget {
const _VideoAttachmentThumbnail({
required this.attachment,
});
final Attachment attachment;
@override
_VideoAttachmentThumbnailState createState() =>
_VideoAttachmentThumbnailState();
}
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.networkUrl(
Uri.parse(widget.attachment.assetUrl!),
)..initialize().then((_) {
// ignore: no-empty-block
setState(() {}); //when your thumbnail will show.
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
width: 32,
child: _controller.value.isInitialized
? VideoPlayer(_controller)
: const CircularProgressIndicator.adaptive(),
);
return {
AttachmentType.image: _createMediaThumbnail,
AttachmentType.giphy: _createMediaThumbnail,
AttachmentType.video: _createMediaThumbnail,
AttachmentType.urlPreview: _createUrlThumbnail,
AttachmentType.file: _createFileThumbnail,
};
}
}
@@ -1169,7 +1169,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
}
final containsUrl = quotedMessage.attachments.any((it) {
return it.titleLink != null;
return it.type == AttachmentType.urlPreview;
});
return StreamQuotedMessageWidget(
@@ -1316,8 +1316,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
message = message.copyWith(text: '/${message.command} ${message.text}');
}
final skipEnrichUrl = _effectiveController.ogAttachment == null;
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled;
@@ -1341,10 +1339,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
await WidgetsBinding.instance.endOfFrame;
}
await _sendOrUpdateMessage(
message: message,
skipEnrichUrl: skipEnrichUrl,
);
await _sendOrUpdateMessage(message: message);
if (mounted) {
if (shouldKeepFocus) {
@@ -1357,36 +1352,29 @@ class StreamMessageInputState extends State<StreamMessageInput>
Future<void> _sendOrUpdateMessage({
required Message message,
bool skipEnrichUrl = false,
}) async {
final channel = StreamChannel.of(context).channel;
try {
Future sendingFuture;
if (_isEditing) {
sendingFuture = channel.updateMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
sendingFuture = channel.updateMessage(message);
} else {
sendingFuture = channel.sendMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
sendingFuture = channel.sendMessage(message);
}
final resp = await sendingFuture;
if (resp.message?.type == 'error') {
if (resp.message?.isError ?? false) {
_effectiveController.message = message;
}
_startSlowMode();
widget.onMessageSent?.call(resp.message);
} catch (e, stk) {
if (widget.onError != null) {
widget.onError?.call(e, stk);
} else {
rethrow;
return widget.onError?.call(e, stk);
}
rethrow;
}
}
@@ -1,14 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
import 'package:stream_chat_flutter/src/attachment/file_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../attachment/thumbnail/video_attachment_thumbnail.dart';
/// WidgetBuilder used to build the message input attachment list.
///
/// see more:
@@ -93,7 +90,7 @@ class _StreamMessageInputAttachmentListState
// Split the attachments into file and media attachments.
for (final attachment in widget.attachments) {
if (attachment.type == 'file') {
if (attachment.type == AttachmentType.file) {
fileAttachments.add(attachment);
} else {
mediaAttachments.add(attachment);
@@ -123,7 +120,7 @@ class _StreamMessageInputAttachmentListState
}
return SingleChildScrollView(
padding: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.only(top: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@@ -203,23 +200,19 @@ class MessageInputFileAttachments extends StatelessWidget {
}
// Otherwise, use the default builder.
return ClipRRect(
key: Key(attachment.id),
borderRadius: BorderRadius.circular(10),
child: StreamFileAttachment(
message: Message(), // dummy message
file: attachment,
constraints: BoxConstraints.loose(Size(
MediaQuery.of(context).size.width * 0.65,
56,
)),
trailing: Padding(
padding: const EdgeInsets.all(8),
child: RemoveAttachmentButton(
onPressed: onRemovePressed != null
? () => onRemovePressed!(attachment)
: null,
),
return StreamFileAttachment(
message: Message(), // Dummy message
file: attachment,
constraints: BoxConstraints.loose(Size(
MediaQuery.of(context).size.width * 0.65,
56,
)),
trailing: Padding(
padding: const EdgeInsets.all(8),
child: RemoveAttachmentButton(
onPressed: onRemovePressed != null
? () => onRemovePressed!(attachment)
: null,
),
),
);
@@ -258,7 +251,8 @@ class MessageInputMediaAttachments extends StatelessWidget {
height: 104,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
cacheExtent: 104 * 10, // Cache 10 items ahead.
children: attachments.map<Widget>(
(attachment) {
// If a custom builder is provided, use it.
@@ -267,27 +261,47 @@ class MessageInputMediaAttachments extends StatelessWidget {
return builder(context, attachment, onRemovePressed);
}
return ClipRRect(
final colorTheme = StreamChatTheme.of(context).colorTheme;
final shape = RoundedRectangleBorder(
side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(14),
);
return Container(
key: Key(attachment.id),
borderRadius: BorderRadius.circular(10),
child: Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1,
child: MessageInputMediaAttachmentThumbnail(
attachment: attachment,
clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration(shape: shape),
child: AspectRatio(
aspectRatio: 1,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
StreamMediaAttachmentThumbnail(
media: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
top: 8,
right: 8,
child: RemoveAttachmentButton(
onPressed: onRemovePressed != null
? () => onRemovePressed!(attachment)
: null,
if (attachment.type == AttachmentType.video)
Positioned(
left: 8,
bottom: 8,
child: StreamSvgIcon.videoCall(),
),
Positioned(
top: 8,
right: 8,
child: RemoveAttachmentButton(
onPressed: onRemovePressed != null
? () => onRemovePressed!(attachment)
: null,
),
),
),
],
],
),
),
);
},
@@ -297,61 +311,6 @@ class MessageInputMediaAttachments extends StatelessWidget {
}
}
/// A widget that displays a thumbnail for a media attachment.
class MessageInputMediaAttachmentThumbnail extends StatelessWidget {
/// Creates a new media attachment widget.
const MessageInputMediaAttachmentThumbnail({
super.key,
required this.attachment,
});
/// The attachment to display.
final Attachment attachment;
@override
Widget build(BuildContext context) {
switch (attachment.type) {
case 'image':
case 'giphy':
return attachment.file != null
? Image.memory(
attachment.file!.bytes!,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
)
: CachedNetworkImage(
imageUrl: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl!,
fit: BoxFit.cover,
errorWidget: (_, obj, trace) => Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
);
case 'video':
return Stack(
children: [
StreamVideoAttachmentThumbnail(video: attachment),
Positioned(
left: 8,
bottom: 10,
child: StreamSvgIcon.videoCall(),
),
],
);
default:
return const ColoredBox(
color: Colors.black26,
child: Icon(Icons.insert_drive_file),
);
}
}
}
/// Material Button used for removing attachments.
class RemoveAttachmentButton extends StatelessWidget {
/// Creates a new remove attachment button.
@@ -104,6 +104,7 @@ class StreamMessageListView extends StatefulWidget {
this.loadingBuilder,
this.emptyBuilder,
this.systemMessageBuilder,
this.ephemeralMessageBuilder,
this.messageListBuilder,
this.errorBuilder,
this.messageFilter,
@@ -149,6 +150,9 @@ class StreamMessageListView extends StatefulWidget {
/// {@macro systemMessageBuilder}
final SystemMessageBuilder? systemMessageBuilder;
/// {@macro ephemeralMessageBuilder}
final EphemeralMessageBuilder? ephemeralMessageBuilder;
/// {@macro parentMessageBuilder}
final ParentMessageBuilder? parentMessageBuilder;
@@ -915,13 +919,19 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
final hasFileAttachment =
message.attachments.any((it) => it.type == AttachmentType.file);
final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null);
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
final isEphemeral = message.isEphemeral;
final attachmentBorderRadius = hasUrlAttachment
? 8.0
: hasFileAttachment
? 12.0
: 14.0;
final borderSide =
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final borderSide = isOnlyEmoji ? BorderSide.none : null;
final defaultMessageWidget = StreamMessageWidget(
showReplyMessage: false,
@@ -935,13 +945,34 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
showUsername: !isMyMessage,
padding: const EdgeInsets.all(8),
showSendingIndicator: false,
attachmentPadding: EdgeInsets.all(
hasUrlAttachment
? 8
: hasFileAttachment
? 4
: 2,
),
attachmentShape: RoundedRectangleBorder(
side: BorderSide(
color: _streamTheme.colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: isMyMessage
? Radius.circular(attachmentBorderRadius)
: Radius.zero,
topRight: Radius.circular(attachmentBorderRadius),
bottomRight: isMyMessage
? Radius.zero
: Radius.circular(attachmentBorderRadius),
),
),
borderRadiusGeometry: BorderRadius.only(
topLeft: const Radius.circular(16),
bottomLeft:
isMyMessage ? const Radius.circular(16) : const Radius.circular(2),
bottomLeft: isMyMessage ? const Radius.circular(16) : Radius.zero,
topRight: const Radius.circular(16),
bottomRight:
isMyMessage ? const Radius.circular(2) : const Radius.circular(16),
bottomRight: isMyMessage ? Radius.zero : const Radius.circular(16),
),
textPadding: EdgeInsets.symmetric(
vertical: 8,
@@ -1061,14 +1092,8 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
}
if (message.isEphemeral) {
// return widget.ephemeralMessageBuilder?.call(context, message) ??
return StreamEphemeralMessage(
message: message,
// onMessageTap: (message) {
// widget.onEphemeralMessageTap?.call(message);
// FocusScope.of(context).unfocus();
// },
);
return widget.ephemeralMessageBuilder?.call(context, message) ??
StreamEphemeralMessage(message: message);
}
final userId = StreamChat.of(context).currentUser!.id;
@@ -1088,14 +1113,21 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
}
final hasFileAttachment =
message.attachments.any((it) => it.type == 'file');
message.attachments.any((it) => it.type == AttachmentType.file);
final hasUrlAttachment =
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
final isThreadMessage =
message.parentId != null && message.showInChannel == true;
final hasReplies = message.replyCount! > 0;
final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0;
final attachmentBorderRadius = hasUrlAttachment
? 8.0
: hasFileAttachment
? 12.0
: 14.0;
final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
!hasReplies &&
@@ -1119,13 +1151,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final isEphemeral = message.isEphemeral;
final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide =
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final borderSide = isOnlyEmoji ? BorderSide.none : null;
final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? [];
@@ -1170,27 +1196,39 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
showFlagButton: !isMyMessage,
borderSide: borderSide,
onThreadTap: _onThreadTap,
attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: isMyMessage
? Radius.circular(attachmentBorderRadius)
: Radius.circular(
(hasTimeDiff || !isNextUserSame) &&
!(hasReplies || isThreadMessage || hasFileAttachment)
? 0
: attachmentBorderRadius,
),
topRight: Radius.circular(attachmentBorderRadius),
bottomRight: isMyMessage
? Radius.circular(
(hasTimeDiff || !isNextUserSame) &&
!(hasReplies || isThreadMessage || hasFileAttachment)
? 0
: attachmentBorderRadius,
)
: Radius.circular(attachmentBorderRadius),
attachmentShape: RoundedRectangleBorder(
side: BorderSide(
color: _streamTheme.colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: isMyMessage
? Radius.circular(attachmentBorderRadius)
: Radius.circular(
(hasTimeDiff || !isNextUserSame) &&
!(hasReplies || isThreadMessage || hasFileAttachment)
? 0
: attachmentBorderRadius,
),
topRight: Radius.circular(attachmentBorderRadius),
bottomRight: isMyMessage
? Radius.circular(
(hasTimeDiff || !isNextUserSame) &&
!(hasReplies || isThreadMessage || hasFileAttachment)
? 0
: attachmentBorderRadius,
)
: Radius.circular(attachmentBorderRadius),
),
),
attachmentPadding: EdgeInsets.all(
hasUrlAttachment
? 8
: hasFileAttachment
? 4
: 2,
),
attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
borderRadiusGeometry: BorderRadius.only(
topLeft: const Radius.circular(16),
bottomLeft: isMyMessage
@@ -22,6 +22,7 @@ class MessageCard extends StatefulWidget {
required this.isGiphy,
required this.attachmentBuilders,
required this.attachmentPadding,
required this.attachmentShape,
required this.onAttachmentTap,
required this.onShowMessage,
required this.onReplyTap,
@@ -80,6 +81,9 @@ class MessageCard extends StatefulWidget {
/// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -200,6 +204,7 @@ class _MessageCardState extends State<MessageCard> {
message: widget.message,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
attachmentShape: widget.attachmentShape,
onAttachmentTap: widget.onAttachmentTap,
onShowMessage: widget.onShowMessage,
onReplyTap: widget.onReplyTap,
@@ -49,11 +49,9 @@ class StreamMessageWidget extends StatefulWidget {
this.reverse = false,
this.translateUserAvatar = true,
this.shape,
this.attachmentShape,
this.borderSide,
this.attachmentBorderSide,
this.borderRadiusGeometry,
this.attachmentBorderRadiusGeometry,
this.attachmentShape,
this.onMentionTap,
this.onMessageTap,
this.showReactionPicker = true,
@@ -334,21 +332,11 @@ class StreamMessageWidget extends StatefulWidget {
/// {@endtemplate}
final BorderSide? borderSide;
/// {@template attachmentBorderSide}
/// The borderSide of an attachment
/// {@endtemplate}
final BorderSide? attachmentBorderSide;
/// {@template borderRadiusGeometry}
/// The border radius of the message text
/// {@endtemplate}
final BorderRadiusGeometry? borderRadiusGeometry;
/// {@template attachmentBorderRadiusGeometry}
/// The border radius of an attachment
/// {@endtemplate}
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
/// {@template padding}
/// The padding of the widget
/// {@endtemplate}
@@ -542,9 +530,7 @@ class StreamMessageWidget extends StatefulWidget {
ShapeBorder? shape,
ShapeBorder? attachmentShape,
BorderSide? borderSide,
BorderSide? attachmentBorderSide,
BorderRadiusGeometry? borderRadiusGeometry,
BorderRadiusGeometry? attachmentBorderRadiusGeometry,
EdgeInsetsGeometry? padding,
EdgeInsets? textPadding,
EdgeInsetsGeometry? attachmentPadding,
@@ -603,10 +589,7 @@ class StreamMessageWidget extends StatefulWidget {
shape: shape ?? this.shape,
attachmentShape: attachmentShape ?? this.attachmentShape,
borderSide: borderSide ?? this.borderSide,
attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide,
borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry,
attachmentBorderRadiusGeometry:
attachmentBorderRadiusGeometry ?? this.attachmentBorderRadiusGeometry,
padding: padding ?? this.padding,
textPadding: textPadding ?? this.textPadding,
attachmentPadding: attachmentPadding ?? this.attachmentPadding,
@@ -691,8 +674,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
/// {@template isGiphy}
/// `true` if any of the [message]'s attachments are a giphy.
/// {@endtemplate}
bool get isGiphy =>
widget.message.attachments.any((element) => element.type == 'giphy');
bool get isGiphy => widget.message.attachments
.any((element) => element.type == AttachmentType.giphy);
/// {@template isOnlyEmoji}
/// `true` if [message.text] contains only emoji.
@@ -749,7 +732,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
bool get shouldShowEditAction =>
widget.showEditMessage &&
!isDeleteFailed &&
!widget.message.attachments.any((element) => element.type == 'giphy');
!widget.message.attachments
.any((element) => element.type == AttachmentType.giphy);
bool get shouldShowResendAction =>
widget.showResendMessage && (isSendFailed || isUpdateFailed);
@@ -762,7 +746,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
bool get shouldShowEditMessage =>
widget.showEditMessage &&
!isDeleteFailed &&
!widget.message.attachments.any((element) => element.type == 'giphy');
!widget.message.attachments
.any((element) => element.type == AttachmentType.giphy);
bool get shouldShowThreadReplyAction =>
widget.showThreadReplyMessage &&
@@ -853,6 +838,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
attachmentShape: widget.attachmentShape,
onAttachmentTap: widget.onAttachmentTap,
onReplyTap: widget.onReplyTap,
onShowMessage: widget.onShowMessage,
@@ -48,6 +48,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.isGiphy,
required this.attachmentBuilders,
required this.attachmentPadding,
required this.attachmentShape,
required this.onAttachmentTap,
required this.onShowMessage,
required this.onReplyTap,
@@ -150,6 +151,9 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -341,6 +345,7 @@ class MessageWidgetContent extends StatelessWidget {
isGiphy: isGiphy,
attachmentBuilders: attachmentBuilders,
attachmentPadding: attachmentPadding,
attachmentShape: attachmentShape,
onAttachmentTap: onAttachmentTap,
onReplyTap: onReplyTap,
onShowMessage: onShowMessage,
@@ -16,6 +16,7 @@ class ParseAttachments extends StatelessWidget {
required this.message,
required this.attachmentBuilders,
required this.attachmentPadding,
this.attachmentShape,
this.onAttachmentTap,
this.onShowMessage,
this.onReplyTap,
@@ -31,6 +32,9 @@ class ParseAttachments extends StatelessWidget {
/// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -104,6 +108,8 @@ class ParseAttachments extends StatelessWidget {
var builders = attachmentBuilders;
builders ??= StreamAttachmentWidgetBuilder.defaultBuilders(
message: message,
shape: attachmentShape,
padding: attachmentPadding,
onAttachmentTap: onAttachmentTap,
);
@@ -50,45 +50,47 @@ class StreamMessageReactionsModal extends StatelessWidget {
final child = Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
message,
constraints,
fontSize,
orientation,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactionPicker && hasReactionPermission)
LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment(
calculateReactionsHorizontalAlignment(
user,
message,
constraints,
fontSize,
orientation,
),
0,
),
0,
),
child: StreamReactionPicker(
message: message,
),
);
},
),
const SizedBox(height: 10),
IgnorePointer(
child: messageWidget,
),
if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8),
ReactionsCard(
currentUser: user!,
message: message,
messageTheme: messageTheme,
child: StreamReactionPicker(
message: message,
),
);
},
),
const SizedBox(height: 10),
IgnorePointer(
child: messageWidget,
),
if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8),
ReactionsCard(
currentUser: user!,
message: message,
messageTheme: messageTheme,
),
],
],
],
),
),
),
),
@@ -51,7 +51,7 @@ class TextBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (message.text?.trim().isEmpty ?? false) return const Offstage();
if (message.text?.trim().isEmpty ?? true) return const Offstage();
return Padding(
padding: isOnlyEmoji ? EdgeInsets.zero : textPadding,
child: textBuilder != null
@@ -228,8 +228,7 @@ class StreamChannelListTile extends StatelessWidget {
}
final hasNonUrlAttachments = lastMessage.attachments
.where((it) => it.titleLink == null || it.type == 'giphy')
.isNotEmpty;
.any((it) => it.type != AttachmentType.urlPreview);
return Padding(
padding: const EdgeInsets.only(right: 4),
@@ -201,6 +201,7 @@ class StreamChatThemeData {
urlAttachmentTitleStyle: textTheme.footnoteBold,
urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTitleMaxLine: 1,
urlAttachmentTextMaxLine: 3,
),
otherMessageTheme: StreamMessageThemeData(
reactionsBackgroundColor: colorTheme.borders,
@@ -227,6 +228,7 @@ class StreamChatThemeData {
urlAttachmentTitleStyle: textTheme.footnoteBold,
urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTitleMaxLine: 1,
urlAttachmentTextMaxLine: 3,
),
messageInputTheme: StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20),
@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:math';
import 'package:diacritic/diacritic.dart';
@@ -5,6 +6,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_size_getter/file_input.dart'; // For compatibility with flutter web.
import 'package:image_size_getter/image_size_getter.dart' hide Size;
import 'package:stream_chat_flutter/src/localization/translations.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -114,7 +117,7 @@ extension PlatformFileX on PlatformFile {
final file = toAttachmentFile;
final extraDataMap = <String, Object>{};
final mimeType = file.mimeType?.mimeType;
final mimeType = file.mediaType?.mimeType;
if (mimeType != null) {
extraDataMap['mime_type'] = mimeType;
@@ -151,7 +154,7 @@ extension XFileX on XFile {
final extraDataMap = <String, Object>{};
final mimeType = this.mimeType ?? file.mimeType?.mimeType;
final mimeType = this.mimeType ?? file.mediaType?.mimeType;
if (mimeType != null) {
extraDataMap['mime_type'] = mimeType;
@@ -367,7 +370,7 @@ extension MessageX on Message {
/// Returns an approximation of message size
double roughMessageSize(double? fontSize) {
var messageTextLength = min(text!.biggestLine().length, 65);
var messageTextLength = min(text?.biggestLine().length ?? 0, 65);
if (quotedMessage != null) {
var quotedMessageLength =
@@ -488,3 +491,46 @@ extension ConstraintsX on BoxConstraints {
);
}
}
/// Useful extensions on [Attachment].
extension OriginalSizeX on Attachment {
/// Returns the size of the attachment if it is an image or giffy.
/// Otherwise, returns null.
Size? get originalSize {
// Return null if the attachment is not an image or giffy.
if (type != AttachmentType.image && type != AttachmentType.giphy) {
return null;
}
// Calculate size locally if the attachment is not uploaded yet.
final file = this.file;
if (file != null) {
ImageInput? input;
if (file.bytes != null) {
input = MemoryInput(file.bytes!);
} else if (file.path != null) {
input = FileInput(File(file.path!));
}
// Return null if the file does not contain enough information.
if (input == null) return null;
try {
final size = ImageSizeGetter.getSize(input);
if (size.needRotate) {
return Size(size.height.toDouble(), size.width.toDouble());
}
return Size(size.width.toDouble(), size.height.toDouble());
} catch (e, stk) {
debugPrint('Error getting image size: $e\n$stk');
return null;
}
}
// Otherwise, use the size provided by the server.
final width = originalWidth;
final height = originalHeight;
if (width == null || height == null) return null;
return Size(width.toDouble(), height.toDouble());
}
}
@@ -259,6 +259,14 @@ typedef SystemMessageBuilder = Widget Function(
Message,
);
/// {@template ephemeralMessageBuilder}
/// A widget builder for creating custom ephemeral messages.
/// {@endtemplate}
typedef EphemeralMessageBuilder = Widget Function(
BuildContext,
Message,
);
/// {@template threadBuilder}
/// A widget builder for creating custom thread UI.
/// {@endtemplate}
@@ -142,6 +142,8 @@ class StreamVideoThumbnailImage
int get hashCode => Object.hash(video, scale);
@override
String toString() =>
'${objectRuntimeType(this, 'StreamVideoThumbnailImage')}($video, scale: $scale)';
String toString() {
final runtimeType = objectRuntimeType(this, 'StreamVideoThumbnailImage');
return '$runtimeType($video, scale: $scale)';
}
}
@@ -7,9 +7,9 @@ export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
export 'src/attachment/attachment_title.dart';
export 'src/attachment/gallery_attachment.dart';
export 'src/attachment/handler/stream_attachment_handler.dart';
export 'src/attachment/image_attachment.dart';
export 'src/attachment/gallery_attachment.dart';
export 'src/attachment/stream_attachment_package.dart';
export 'src/attachment/url_attachment.dart';
export 'src/attachment/video_attachment.dart';
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../mocks.dart';
@@ -18,6 +19,27 @@ void main() {
final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(themeData);
final attachments = [
Attachment(
type: 'image',
title: 'example.png',
imageUrl:
'https://logowik.com/content/uploads/images/flutter5786.jpg',
extraData: const {
'mime_type': 'png',
},
),
Attachment(
type: 'image',
title: 'example.png',
imageUrl:
'https://logowik.com/content/uploads/images/flutter5786.jpg',
extraData: const {
'mime_type': 'png',
},
),
];
await tester.pumpWidget(
MaterialApp(
home: StreamChatTheme(
@@ -31,26 +53,17 @@ void main() {
300,
)),
message: Message(),
attachments: [
Attachment(
type: 'image',
title: 'example.png',
imageUrl:
'https://logowik.com/content/uploads/images/flutter5786.jpg',
extraData: const {
'mime_type': 'png',
},
),
Attachment(
type: 'image',
title: 'example.png',
imageUrl:
'https://logowik.com/content/uploads/images/flutter5786.jpg',
extraData: const {
'mime_type': 'png',
},
),
],
attachments: attachments,
itemBuilder: (context, index) {
final attachment = attachments[index];
return StreamImageAttachmentThumbnail(
image: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
},
),
),
),
@@ -30,7 +30,7 @@ void main() {
300,
)),
message: Message(),
file: Attachment(
giphy: Attachment(
type: 'giphy',
title: 'example.gif',
imageUrl:
@@ -31,7 +31,7 @@ void main() {
300,
)),
message: Message(),
file: Attachment(
image: Attachment(
type: 'image',
title: 'example.png',
imageUrl:
@@ -26,6 +26,7 @@ void main() {
child: SizedBox(
child: StreamUrlAttachment(
messageTheme: streamTheme.ownMessageTheme,
message: Message(),
hostDisplayName: 'Test',
urlAttachment: Attachment(
title: 'Flutter',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -70,7 +70,7 @@ final _channelThemeControlMidLerp = StreamChannelHeaderThemeData(
width: 40,
),
),
color: const Color(0xff101418),
color: const Color(0xff111417),
titleStyle: const TextStyle(
color: Color(0xffffffff),
fontWeight: FontWeight.bold,
@@ -73,7 +73,7 @@ final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData(
width: 40,
),
),
color: const Color(0xff87898b),
color: const Color(0xff88898a),
titleStyle: const TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
@@ -73,11 +73,7 @@ void main() {
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: StreamGalleryFooter(
mediaAttachmentPackages: Message().getAttachmentPackageList(),
),
);
return const SizedBox.shrink();
},
),
),
@@ -116,11 +112,7 @@ void main() {
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: StreamGalleryFooter(
mediaAttachmentPackages: Message().getAttachmentPackageList(),
),
);
return const SizedBox.shrink();
},
),
),
@@ -160,7 +152,7 @@ final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData(
// Mid-lerp theme control
const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
backgroundColor: Color(0xff87898b),
backgroundColor: Color(0xff88898a),
shareIconColor: Color(0xff7f7f7f),
titleTextStyle: TextStyle(
color: Color(0xff7f7f7f),
@@ -169,7 +161,7 @@ const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
),
gridIconButtonColor: Color(0xff7f7f7f),
bottomSheetBarrierColor: Color(0x4c000000),
bottomSheetBackgroundColor: Color(0xff87898b),
bottomSheetBackgroundColor: Color(0xff88898a),
bottomSheetPhotosTextStyle: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
@@ -66,22 +66,7 @@ void main() {
home: Builder(
builder: (context) {
_context = context;
final attachment = Attachment(
type: 'video',
title: 'video.mp4',
);
final _message = Message(
createdAt: DateTime.now(),
attachments: [
attachment,
],
);
return Scaffold(
appBar: StreamGalleryHeader(
message: _message,
attachment: _message.attachments[0],
),
);
return const SizedBox.shrink();
},
),
),
@@ -116,22 +101,7 @@ void main() {
home: Builder(
builder: (context) {
_context = context;
final attachment = Attachment(
type: 'video',
title: 'video.mp4',
);
final _message = Message(
createdAt: DateTime.now(),
attachments: [
attachment,
],
);
return Scaffold(
appBar: StreamGalleryHeader(
message: _message,
attachment: _message.attachments[0],
),
);
return const SizedBox.shrink();
},
),
),
@@ -175,7 +145,7 @@ final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData(
// Light theme test control.
final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
closeButtonColor: const Color(0xff7f7f7f),
backgroundColor: const Color(0xff87898b),
backgroundColor: const Color(0xff88898a),
iconMenuPointColor: const Color(0xff7f7f7f),
titleTextStyle: const TextStyle(
fontSize: 16,
@@ -194,7 +164,7 @@ final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
// Dark theme test control.
final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData(
closeButtonColor: const Color(0xffffffff),
backgroundColor: const Color(0xff101418),
backgroundColor: const Color(0xff121416),
iconMenuPointColor: const Color(0xffffffff),
titleTextStyle: const TextStyle(
fontSize: 16,
@@ -68,7 +68,7 @@ final _messageInputThemeControl = StreamMessageInputThemeData(
final _messageInputThemeControlMidLerp = StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300),
inputBackgroundColor: const Color(0xff87898b),
inputBackgroundColor: const Color(0xff88898a),
actionButtonColor: const Color(0xff196eff),
actionButtonIdleColor: const Color(0xff7a7a7a),
sendButtonColor: const Color(0xff196eff),
@@ -68,12 +68,7 @@ void main() {
home: Builder(
builder: (BuildContext context) {
_context = context;
return Scaffold(
body: StreamChannel(
channel: MockChannel(),
child: const StreamMessageListView(),
),
);
return const SizedBox.shrink();
},
),
),
@@ -98,12 +93,7 @@ void main() {
home: Builder(
builder: (BuildContext context) {
_context = context;
return Scaffold(
body: StreamChannel(
channel: MockChannel(),
child: const StreamMessageListView(),
),
);
return const SizedBox.shrink();
},
),
),
@@ -151,7 +141,7 @@ final _messageListViewThemeDataControl = StreamMessageListViewThemeData(
);
const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData(
backgroundColor: Color(0xff87898b),
backgroundColor: Color(0xff88898a),
);
final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData(