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(); final cancelToken = CancelToken();
Future<SendAttachmentResponse> future; Future<SendAttachmentResponse> future;
if (isImage) { if (isImage) {
@@ -16,6 +16,7 @@ mixin AttachmentType {
static const file = 'file'; static const file = 'file';
static const giphy = 'giphy'; static const giphy = 'giphy';
static const video = 'video'; static const video = 'video';
static const audio = 'audio';
/// Application custom types. /// Application custom types.
static const urlPreview = 'url_preview'; static const urlPreview = 'url_preview';
@@ -53,19 +54,15 @@ class Attachment extends Equatable {
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
_type = type, _type = type,
title = title ?? file?.name, title = title ?? file?.name,
_uploadState = uploadState,
localUri = file?.path != null ? Uri.parse(file!.path!) : null, localUri = file?.path != null ? Uri.parse(file!.path!) : null,
// For backwards compatibility, // For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData]. // set 'file_size', 'mime_type' in [extraData].
extraData = { extraData = {
...extraData, ...extraData,
if (file?.size != null) 'file_size': file?.size, if (file?.size != null) 'file_size': file?.size,
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType, if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType,
} { };
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
/// Create a new instance from a json /// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) => factory Attachment.fromJson(Map<String, dynamic> json) =>
@@ -82,7 +79,8 @@ class Attachment extends Equatable {
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) => factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
Attachment( Attachment(
type: ogAttachment.type, // If the type is not specified, we default to urlPreview.
type: ogAttachment.type ?? AttachmentType.urlPreview,
title: ogAttachment.title, title: ogAttachment.title,
titleLink: ogAttachment.titleLink, titleLink: ogAttachment.titleLink,
text: ogAttachment.text, text: ogAttachment.text,
@@ -98,7 +96,9 @@ class Attachment extends Equatable {
///The attachment type based on the URL resource. This can be: audio, ///The attachment type based on the URL resource. This can be: audio,
///image or video ///image or video
String? get type { 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; return AttachmentType.urlPreview;
} }
@@ -107,6 +107,9 @@ class Attachment extends Equatable {
final String? _type; final String? _type;
/// The raw attachment type.
String? get rawType => _type;
///The link to which the attachment message points to. ///The link to which the attachment message points to.
final String? titleLink; final String? titleLink;
@@ -159,7 +162,15 @@ class Attachment extends Equatable {
final AttachmentFile? file; final AttachmentFile? file;
/// The current upload state of the attachment /// 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 /// Map of custom channel extraData
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
@@ -62,7 +62,7 @@ class AttachmentFile {
String? get extension => name?.split('.').last; String? get extension => name?.split('.').last;
/// The mime type of this file. /// The mime type of this file.
MediaType? get mimeType => name?.mimeType; MediaType? get mediaType => name?.mediaType;
/// Serialize to json /// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this); Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
@@ -75,13 +75,13 @@ class AttachmentFile {
multiPartFile = MultipartFile.fromBytes( multiPartFile = MultipartFile.fromBytes(
bytes!, bytes!,
filename: name, filename: name,
contentType: mimeType, contentType: mediaType,
); );
} else { } else {
multiPartFile = await MultipartFile.fromFile( multiPartFile = await MultipartFile.fromFile(
path!, path!,
filename: name, filename: name,
contentType: mimeType, contentType: mediaType,
); );
} }
return multiPartFile; return multiPartFile;
@@ -20,8 +20,8 @@ extension MapX<K, V> on Map<K?, V?> {
/// Useful extension functions for [String] /// Useful extension functions for [String]
extension StringX on String { extension StringX on String {
/// returns the mime type from the passed file name. /// returns the media type from the passed file name.
MediaType? get mimeType { MediaType? get mediaType {
if (toLowerCase().endsWith('heic')) { if (toLowerCase().endsWith('heic')) {
return MediaType.parse('image/heic'); return MediaType.parse('image/heic');
} else { } else {
@@ -25,13 +25,13 @@ void main() {
group('mimeType', () { group('mimeType', () {
test('should return null if `String` is not a filename', () { test('should return null if `String` is not a filename', () {
const fileName = 'not-a-file-name'; const fileName = 'not-a-file-name';
final mimeType = fileName.mimeType; final mimeType = fileName.mediaType;
expect(mimeType, isNull); expect(mimeType, isNull);
}); });
test('should return mimeType if string is a filename', () { test('should return mimeType if string is a filename', () {
const fileName = 'dummyFileName.jpeg'; const fileName = 'dummyFileName.jpeg';
final mimeType = fileName.mimeType; final mimeType = fileName.mediaType;
expect(mimeType, isNotNull); expect(mimeType, isNotNull);
expect(mimeType!.type, 'image'); expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'jpeg'); expect(mimeType.subtype, 'jpeg');
@@ -39,7 +39,7 @@ void main() {
test('should return `image/heic` if ends with `heic`', () { test('should return `image/heic` if ends with `heic`', () {
const fileName = 'dummyFileName.heic'; const fileName = 'dummyFileName.heic';
final mimeType = fileName.mimeType; final mimeType = fileName.mediaType;
expect(mimeType, isNotNull); expect(mimeType, isNotNull);
expect(mimeType!.type, 'image'); expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'heic'); expect(mimeType.subtype, 'heic');
@@ -57,6 +57,8 @@ class AttachmentWidgetCatalog {
extension on List<Attachment> { extension on List<Attachment> {
/// Groups the attachments by their type. /// Groups the attachments by their type.
Map<String, List<Attachment>> get grouped { 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: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/gallery_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/giphy_attachment.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.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/url_attachment.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/stream_chat.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.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/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../attachment_upload_state_builder.dart';
part 'fallback_attachment_builder.dart'; part 'fallback_attachment_builder.dart';
part 'file_attachment_builder.dart'; part 'file_attachment_builder.dart';
@@ -82,41 +75,58 @@ abstract class StreamAttachmentWidgetBuilder {
/// widget. /// widget.
static List<StreamAttachmentWidgetBuilder> defaultBuilders({ static List<StreamAttachmentWidgetBuilder> defaultBuilders({
required Message message, required Message message,
ShapeBorder? shape,
EdgeInsetsGeometry padding = const EdgeInsets.all(4),
StreamAttachmentWidgetTapCallback? onAttachmentTap, StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) { }) {
return [ return [
// Handles a mix of image, gif, video, and file attachments. // Handles a mix of image, gif, video, url and file attachments.
MixedAttachmentBuilder( MixedAttachmentBuilder(
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// Handles a mix of image, gif, and video attachments. // Handles a mix of image, gif, and video attachments.
GalleryAttachmentBuilder( GalleryAttachmentBuilder(
shape: shape,
padding: padding,
runSpacing: padding.vertical / 2,
spacing: padding.horizontal / 2,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// Handles file attachments. // Handles file attachments.
FileAttachmentBuilder( FileAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// Handles giphy attachments. // Handles giphy attachments.
GiphyAttachmentBuilder( GiphyAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// Handles image attachments. // Handles image attachments.
ImageAttachmentBuilder( ImageAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// Handles video attachments. // Handles video attachments.
VideoAttachmentBuilder( VideoAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
// We don't handle URL attachments if the message is a reply. // We don't handle URL attachments if the message is a reply.
if (message.quotedMessage == null) if (message.quotedMessage == null)
UrlAttachmentBuilder( UrlAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
), ),
@@ -93,16 +93,6 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
attachments: galleryAttachments, attachments: galleryAttachments,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final attachment = galleryAttachments[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; VoidCallback? onTap;
if (onAttachmentTap != null) { if (onAttachmentTap != null) {
@@ -112,29 +102,13 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Stack( child: Stack(
alignment: Alignment.center,
children: [ children: [
if (isImage) StreamMediaAttachmentThumbnail(
StreamImageAttachmentThumbnail( media: attachment,
image: attachment, width: constraints.maxWidth,
width: double.infinity, height: constraints.maxHeight,
height: double.infinity, fit: BoxFit.cover,
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,
),
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder( child: StreamAttachmentUploadStateBuilder(
@@ -62,6 +62,7 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: StreamImageAttachment( child: StreamImageAttachment(
shape: shape,
message: message, message: message,
constraints: constraints, constraints: constraints,
image: image, image: image,
@@ -3,67 +3,71 @@ part of 'attachment_widget_builder.dart';
/// {@template mixedAttachmentBuilder} /// {@template mixedAttachmentBuilder}
/// A widget builder for Mixed attachment type. /// A widget builder for Mixed attachment type.
/// ///
/// This builder is used when a message contains both image/video/giphy and file /// This builder is used when a message contains a mix of media type and file
/// attachments. /// or url preview attachments.
/// ///
/// This builder will render first image/video/giphy attachment and then render /// This builder will render first the url preview or file attachment and then
/// the file attachments. /// the media attachments.
/// {@endtemplate} /// {@endtemplate}
class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder { class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
/// {@macro mixedAttachmentBuilder} /// {@macro mixedAttachmentBuilder}
MixedAttachmentBuilder({ MixedAttachmentBuilder({
this.shape,
this.padding = const EdgeInsets.all(4), this.padding = const EdgeInsets.all(4),
this.onAttachmentTap, StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) : _imageAttachmentBuilder = ImageAttachmentBuilder( }) : _imageAttachmentBuilder = ImageAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
), ),
_videoAttachmentBuilder = VideoAttachmentBuilder( _videoAttachmentBuilder = VideoAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
), ),
_giphyAttachmentBuilder = GiphyAttachmentBuilder( _giphyAttachmentBuilder = GiphyAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
), ),
_galleryAttachmentBuilder = GalleryAttachmentBuilder( _galleryAttachmentBuilder = GalleryAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
), ),
_fileAttachmentBuilder = FileAttachmentBuilder( _fileAttachmentBuilder = FileAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
),
_urlAttachmentBuilder = UrlAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
); );
/// The shape of the gallery attachment. /// The padding to apply to the mixed attachment widget.
final ShapeBorder? shape;
/// The padding to apply to the gallery attachment widget.
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The callback to call when the attachment is tapped.
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder; late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder; late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder; late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder; late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder; late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _urlAttachmentBuilder;
@override @override
bool canHandle( bool canHandle(
Message message, Message message,
Map<String, List<Attachment>> attachments, Map<String, List<Attachment>> attachments,
) { ) {
final containsImage = attachments.keys.contains(AttachmentType.image); final types = attachments.keys;
final containsVideo = attachments.keys.contains(AttachmentType.video);
final containsGiphy = attachments.keys.contains(AttachmentType.giphy); final containsImage = types.contains(AttachmentType.image);
final containsFile = attachments.keys.contains(AttachmentType.file); 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; final containsMedia = containsImage || containsVideo || containsGiphy;
return containsMedia && containsFile; return containsMedia && containsFile ||
containsMedia && containsUrlPreview ||
containsFile && containsUrlPreview ||
containsMedia && containsFile && containsUrlPreview;
} }
@override @override
@@ -74,6 +78,7 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
) { ) {
assert(debugAssertCanHandle(message, attachments), ''); assert(debugAssertCanHandle(message, attachments), '');
final urls = attachments[AttachmentType.urlPreview];
final files = attachments[AttachmentType.file]; final files = attachments[AttachmentType.file];
final images = attachments[AttachmentType.image]; final images = attachments[AttachmentType.image];
final videos = attachments[AttachmentType.video]; final videos = attachments[AttachmentType.video];
@@ -86,11 +91,14 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
if (urls != null)
_urlAttachmentBuilder.build(context, message, {
AttachmentType.urlPreview: urls,
}),
if (files != null) if (files != null)
for (final file in files) _fileAttachmentBuilder.build(context, message, {
_fileAttachmentBuilder.build(context, message, { AttachmentType.file: files,
AttachmentType.file: [file], }),
}),
if (shouldBuildGallery) if (shouldBuildGallery)
_galleryAttachmentBuilder.build(context, message, { _galleryAttachmentBuilder.build(context, message, {
if (images != null) AttachmentType.image: images, if (images != null) AttachmentType.image: images,
@@ -128,19 +128,21 @@ class _FileTypeImage extends StatelessWidget {
file: file, file: file,
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
// fit: BoxFit.cover,
); );
final mimeType = file.title?.mimeType?.type; final mediaType = file.title?.mediaType;
final isImage = mimeType == 'image'; final isImage = mediaType?.type == AttachmentType.image;
final isVideo = mimeType == 'video'; final isVideo = mediaType?.type == AttachmentType.video;
if (isImage || isVideo) { if (isImage || isVideo) {
final colorTheme = StreamChatTheme.of(context).colorTheme; final colorTheme = StreamChatTheme.of(context).colorTheme;
child = Container( child = Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration( decoration: ShapeDecoration(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(color: colorTheme.borders), side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
@@ -127,6 +127,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1], [1],
[1], [1],
], ],
spacing: spacing,
runSpacing: runSpacing,
children: [ children: [
itemBuilder(context, 0), itemBuilder(context, 0),
itemBuilder(context, 1), itemBuilder(context, 1),
@@ -145,6 +147,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: const [ pattern: const [
[1, 1], [1, 1],
], ],
spacing: spacing,
runSpacing: runSpacing,
children: [ children: [
itemBuilder(context, 0), itemBuilder(context, 0),
itemBuilder(context, 1), itemBuilder(context, 1),
@@ -170,6 +174,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: [ pattern: [
if (isLandscape1) [2, 1] else [1, 2], if (isLandscape1) [2, 1] else [1, 2],
], ],
spacing: spacing,
runSpacing: runSpacing,
children: [ children: [
itemBuilder(context, 0), itemBuilder(context, 0),
itemBuilder(context, 1), itemBuilder(context, 1),
@@ -204,6 +210,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1], [1],
[1, 1], [1, 1],
], ],
spacing: spacing,
runSpacing: runSpacing,
reverse: !isLandscape1, reverse: !isLandscape1,
children: [ children: [
itemBuilder(context, 0), itemBuilder(context, 0),
@@ -238,6 +246,8 @@ class StreamGalleryAttachment extends StatelessWidget {
return FlexGrid( return FlexGrid(
pattern: pattern, pattern: pattern,
maxChildren: 4, maxChildren: 4,
spacing: spacing,
runSpacing: runSpacing,
children: children, children: children,
overlayBuilder: (context, remaining) { overlayBuilder: (context, remaining) {
return IgnorePointer( return IgnorePointer(
@@ -12,7 +12,7 @@ class StreamGiphyAttachment extends StatelessWidget {
super.key, super.key,
required this.message, required this.message,
required this.giphy, required this.giphy,
this.type = GiphyInfoType.fixedHeightDownsampled, this.type = GiphyInfoType.original,
this.shape, this.shape,
this.constraints = const BoxConstraints(), this.constraints = const BoxConstraints(),
}); });
@@ -50,18 +50,18 @@ Future<AttachmentData> downloadAttachmentData(
String? downloadUrl; String? downloadUrl;
String? fileName; String? fileName;
/* ---IMAGES/GIFS--- */ /* ---IMAGES/GIFS--- */
if (type == 'image') { if (type == AttachmentType.image) {
downloadUrl = attachment.imageUrl ?? attachment.assetUrl; downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
fileName = attachment.title; fileName = attachment.title;
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}'; fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
} }
/* ---GIPHY's--- */ /* ---GIPHY's--- */
else if (type == 'giphy') { else if (type == AttachmentType.giphy) {
downloadUrl = attachment.thumbUrl; downloadUrl = attachment.thumbUrl;
fileName = '${attachment.title}.gif'; fileName = '${attachment.title}.gif';
} }
/* ---FILES AND VIDEOS--- */ /* ---FILES AND VIDEOS--- */
else if (type == 'file' || type == 'video') { else if (type == AttachmentType.file || type == AttachmentType.video) {
downloadUrl = attachment.assetUrl; downloadUrl = attachment.assetUrl;
fileName = attachment.title; fileName = attachment.title;
} }
@@ -50,9 +50,9 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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) { if (isImage) {
return StreamImageAttachmentThumbnail( return StreamImageAttachmentThumbnail(
image: file, image: file,
@@ -62,7 +62,7 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
); );
} }
final isVideo = mimeType == 'video'; final isVideo = mediaType?.type == AttachmentType.video;
if (isVideo) { if (isVideo) {
return StreamVideoAttachmentThumbnail( return StreamVideoAttachmentThumbnail(
video: file, video: file,
@@ -73,6 +73,6 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
} }
// Return a generic file type icon. // Return a generic file type icon.
return getFileTypeImage(mimeType); return getFileTypeImage(mediaType?.mimeType);
} }
} }
@@ -50,6 +50,9 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget {
return ThumbnailError( return ThumbnailError(
error: error, error: error,
stackTrace: stackTrace, 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:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.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/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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} /// {@template imageAttachmentThumbnail}
/// Widget for building image attachment thumbnail. /// Widget for building image attachment thumbnail.
/// ///
@@ -101,6 +64,9 @@ class StreamImageAttachmentThumbnail extends StatelessWidget {
return ThumbnailError( return ThumbnailError(
error: error, error: error,
stackTrace: stackTrace, 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, super.key,
required this.error, required this.error,
this.stackTrace, 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. /// The error that triggered this error widget.
final Object error; final Object error;
@@ -33,7 +45,9 @@ class ThumbnailError extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Image.asset( return Image.asset(
'images/placeholder.png', 'images/placeholder.png',
fit: BoxFit.cover, width: width,
height: height,
fit: fit,
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
); );
} }
@@ -46,6 +46,9 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget {
return ThumbnailError( return ThumbnailError(
error: error, error: error,
stackTrace: stackTrace, stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
); );
} }
@@ -47,7 +47,7 @@ class StreamUrlAttachment extends StatelessWidget {
color: colorTheme.borders, color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside, strokeAlign: BorderSide.strokeAlignOutside,
), ),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(8),
); );
final backgroundColor = messageTheme.urlAttachmentBackgroundColor; final backgroundColor = messageTheme.urlAttachmentBackgroundColor;
@@ -62,44 +62,43 @@ class StreamUrlAttachment extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (urlAttachment.imageUrl != null) Stack(
Stack( children: [
children: [ AspectRatio(
AspectRatio( // Default aspect ratio for Open Graph images.
// 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
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage aspectRatio: 1.91 / 1,
aspectRatio: 1.91 / 1, child: StreamImageAttachmentThumbnail(
child: StreamImageAttachmentThumbnail( image: urlAttachment,
image: urlAttachment, fit: BoxFit.cover,
fit: BoxFit.cover,
),
), ),
Positioned( ),
left: 0, Positioned(
bottom: 0, left: 0,
child: DecoratedBox( bottom: 0,
decoration: BoxDecoration( child: DecoratedBox(
borderRadius: const BorderRadius.only( decoration: BoxDecoration(
topRight: Radius.circular(16), borderRadius: const BorderRadius.only(
), topRight: Radius.circular(16),
color: backgroundColor,
), ),
child: Padding( color: backgroundColor,
padding: const EdgeInsets.only( ),
top: 8, child: Padding(
left: 8, padding: const EdgeInsets.only(
right: 12, top: 8,
bottom: 4, left: 8,
), right: 12,
child: Text( bottom: 4,
hostDisplayName, ),
style: messageTheme.urlAttachmentHostStyle, child: Text(
), hostDisplayName,
style: messageTheme.urlAttachmentHostStyle,
), ),
), ),
), ),
], ),
), ],
),
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Column( child: Column(
@@ -129,7 +129,7 @@ class AttachmentActionsModal extends StatelessWidget {
if (showSave) if (showSave)
_buildButton( _buildButton(
context, context,
attachment.type == 'video' attachment.type == AttachmentType.video
? context.translations.saveVideoLabel ? context.translations.saveVideoLabel
: context.translations.saveImageLabel, : context.translations.saveImageLabel,
StreamSvgIcon.iconSave( StreamSvgIcon.iconSave(
@@ -36,11 +36,11 @@ class StreamMessagePreviewText extends StatelessWidget {
final messageTextParts = [ final messageTextParts = [
...messageAttachments.map((it) { ...messageAttachments.map((it) {
if (it.type == 'image') { if (it.type == AttachmentType.image) {
return '📷'; return '📷';
} else if (it.type == 'video') { } else if (it.type == AttachmentType.video) {
return '🎬'; return '🎬';
} else if (it.type == 'giphy') { } else if (it.type == AttachmentType.giphy) {
return '[GIF]'; return '[GIF]';
} }
return it == message.attachments.last return it == message.attachments.last
@@ -7,7 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.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/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/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/src/fullscreen_media/full_screen_media_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -272,7 +272,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
} }
} }
if (widget.autoplayVideos && if (widget.autoplayVideos &&
currentAttachment.type == 'video') { currentAttachment.type == AttachmentType.video) {
final controller = videoPackages[currentAttachment.id]!; final controller = videoPackages[currentAttachment.id]!;
controller._chewieController?.play(); controller._chewieController?.play();
} }
@@ -294,47 +294,19 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
child: ContextMenuArea( child: ContextMenuArea(
verticalPadding: 0, verticalPadding: 0,
builder: (_) => [ builder: (_) => [
DownloadMenuItem( DownloadMenuItem(attachment: attachment),
attachment: attachment,
),
], ],
child: PhotoView.customChild( 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, maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
// heroAttributes: PhotoViewHeroAttributes(
// tag: widget.mediaAttachmentPackages,
// ),
backgroundDecoration: const BoxDecoration( backgroundDecoration: const BoxDecoration(
color: Colors.transparent, color: Colors.transparent,
), ),
child: StreamMediaAttachmentThumbnail(
media: attachment,
width: double.infinity,
height: double.infinity,
),
), ),
), ),
), ),
@@ -353,9 +325,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
child: ContextMenuArea( child: ContextMenuArea(
verticalPadding: 0, verticalPadding: 0,
builder: (_) => [ builder: (_) => [
DownloadMenuItem( DownloadMenuItem(attachment: attachment),
attachment: attachment,
),
], ],
child: Chewie( child: Chewie(
controller: controller.chewieController!, controller: controller.chewieController!,
@@ -94,7 +94,7 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
_pageController = PageController(initialPage: widget.startIndex); _pageController = PageController(initialPage: widget.startIndex);
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
final attachment = widget.mediaAttachmentPackages[i].attachment; final attachment = widget.mediaAttachmentPackages[i].attachment;
if (attachment.type != 'video') continue; if (attachment.type != AttachmentType.video) continue;
final package = DesktopVideoPackage(attachment); final package = DesktopVideoPackage(attachment);
videoPackages[attachment.id] = package; videoPackages[attachment.id] = package;
} }
@@ -298,7 +298,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
p.player.pause(); p.player.pause();
} }
} }
if (widget.autoplayVideos && currentAttachment.type == 'video') { if (widget.autoplayVideos &&
currentAttachment.type == AttachmentType.video) {
final package = videoPackages[currentAttachment.id]!; final package = videoPackages[currentAttachment.id]!;
package.player.play(); package.player.play();
} }
@@ -307,7 +308,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
final currentAttachmentPackage = final currentAttachmentPackage =
widget.mediaAttachmentPackages[index]; widget.mediaAttachmentPackages[index];
final attachment = currentAttachmentPackage.attachment; final attachment = currentAttachmentPackage.attachment;
if (attachment.type == 'image' || attachment.type == 'giphy') { if (attachment.type == AttachmentType.image ||
attachment.type == AttachmentType.giphy) {
final imageUrl = attachment.imageUrl ?? final imageUrl = attachment.imageUrl ??
attachment.assetUrl ?? attachment.assetUrl ??
attachment.thumbUrl; 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]!; final package = videoPackages[attachment.id]!;
package.player.open( package.player.open(
Playlist( Playlist(
@@ -95,7 +95,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
final url = attachment.imageUrl ?? final url = attachment.imageUrl ??
attachment.assetUrl ?? attachment.assetUrl ??
attachment.thumbUrl!; attachment.thumbUrl!;
final type = attachment.type == 'image' final type = attachment.type == AttachmentType.image
? 'jpg' ? 'jpg'
: url.split('?').first.split('.').last; : url.split('?').first.split('.').last;
final request = await HttpClient().getUrl(Uri.parse(url)); final request = await HttpClient().getUrl(Uri.parse(url));
@@ -218,7 +218,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
widget.mediaAttachmentPackages[index]; widget.mediaAttachmentPackages[index];
final attachment = attachmentPackage.attachment; final attachment = attachmentPackage.attachment;
final message = attachmentPackage.message; final message = attachmentPackage.message;
if (attachment.type == 'video') { if (attachment.type == AttachmentType.video) {
media = MouseRegion( media = MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
@@ -114,119 +114,121 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final child = Center( final child = Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: SafeArea(
padding: const EdgeInsets.all(8), child: Padding(
child: Column( padding: const EdgeInsets.all(8),
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.stretch,
if (widget.showReactionPicker && hasReactionPermission) children: <Widget>[
LayoutBuilder( if (widget.showReactionPicker && hasReactionPermission)
builder: (context, constraints) { LayoutBuilder(
return Align( builder: (context, constraints) {
alignment: Alignment( return Align(
calculateReactionsHorizontalAlignment( alignment: Alignment(
user, calculateReactionsHorizontalAlignment(
widget.message, user,
constraints, widget.message,
fontSize, constraints,
orientation, 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), const SizedBox(height: 8),
IgnorePointer( Padding(
child: widget.messageWidget, padding: EdgeInsets.only(
), left: widget.reverse ? 0 : 40,
const SizedBox(height: 8), ),
Padding( child: SizedBox(
padding: EdgeInsets.only( width: mediaQueryData.size.width * 0.75,
left: widget.reverse ? 0 : 40, child: Material(
), color: streamChatThemeData.colorTheme.appBg,
child: SizedBox( clipBehavior: Clip.hardEdge,
width: mediaQueryData.size.width * 0.75, shape: RoundedRectangleBorder(
child: Material( borderRadius: BorderRadius.circular(16),
color: streamChatThemeData.colorTheme.appBg, ),
clipBehavior: Clip.hardEdge, child: Column(
shape: RoundedRectangleBorder( crossAxisAlignment: CrossAxisAlignment.stretch,
borderRadius: BorderRadius.circular(16), children: [
), if (widget.showReplyMessage &&
child: Column( widget.message.state.isCompleted)
crossAxisAlignment: CrossAxisAlignment.stretch, ReplyButton(
children: [ onTap: () {
if (widget.showReplyMessage && Navigator.of(context).pop();
widget.message.state.isCompleted) if (widget.onReplyTap != null) {
ReplyButton( widget.onReplyTap?.call(widget.message);
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 extraDataMap = <String, Object>{};
final mimeType = file.mimeType?.mimeType; final mimeType = file.mediaType?.mimeType;
if (mimeType != null) { if (mimeType != null) {
extraDataMap['mime_type'] = mimeType; extraDataMap['mime_type'] = mimeType;
@@ -240,10 +240,10 @@ class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption {
extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController { extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController {
/// Returns the list of available attachment picker options. /// Returns the list of available attachment picker options.
Set<AttachmentPickerType> get currentAttachmentPickerTypes { Set<AttachmentPickerType> get currentAttachmentPickerTypes {
final containsImage = value.any((it) => it.type == 'image'); final containsImage = value.any((it) => it.type == AttachmentType.image);
final containsVideo = value.any((it) => it.type == 'video'); final containsVideo = value.any((it) => it.type == AttachmentType.video);
final containsAudio = value.any((it) => it.type == 'audio'); final containsAudio = value.any((it) => it.type == AttachmentType.audio);
final containsFile = value.any((it) => it.type == 'file'); final containsFile = value.any((it) => it.type == AttachmentType.file);
return { return {
if (containsImage) AttachmentPickerType.images, if (containsImage) AttachmentPickerType.images,
@@ -1,10 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.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/src/message_input/clear_input_item_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
typedef _Builders = Map<String, QuotedMessageAttachmentThumbnailBuilder>;
/// {@template streamQuotedMessage} /// {@template streamQuotedMessage}
/// Widget for the quoted message. /// Widget for the quoted message.
@@ -40,8 +41,7 @@ class StreamQuotedMessageWidget extends StatelessWidget {
final int textLimit; final int textLimit;
/// Map that defines a thumbnail builder for an attachment type /// Map that defines a thumbnail builder for an attachment type
final Map<String, QuotedMessageAttachmentThumbnailBuilder>? final _Builders? attachmentThumbnailBuilders;
attachmentThumbnailBuilders;
/// Padding around the widget /// Padding around the widget
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@@ -109,19 +109,17 @@ class _QuotedMessage extends StatelessWidget {
final bool reverse; final bool reverse;
final Widget Function(BuildContext, Message)? textBuilder; final Widget Function(BuildContext, Message)? textBuilder;
/// Map that defines a thumbnail builder for an attachment type final _Builders? attachmentThumbnailBuilders;
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
attachmentThumbnailBuilders;
bool get _hasAttachments => message.attachments.isNotEmpty; bool get _hasAttachments => message.attachments.isNotEmpty;
bool get _containsText => message.text?.isNotEmpty == true; bool get _containsText => message.text?.isNotEmpty == true;
bool get _containsLinkAttachment => bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null); message.attachments.any((it) => it.type == AttachmentType.urlPreview);
bool get _isGiphy => bool get _isGiphy => message.attachments
message.attachments.any((element) => element.type == 'giphy'); .any((element) => element.type == AttachmentType.giphy);
bool get _isDeleted => message.isDeleted || message.deletedAt != null; bool get _isDeleted => message.isDeleted || message.deletedAt != null;
@@ -150,14 +148,6 @@ class _QuotedMessage extends StatelessWidget {
} else { } else {
// Show quoted message // Show quoted message
children = [ children = [
if (onQuotedMessageClear != null)
PlatformWidgetBuilder(
web: (context, child) => child,
desktop: (context, child) => child,
child: ClearInputItemButton(
onTap: onQuotedMessageClear,
),
),
if (_hasAttachments) if (_hasAttachments)
_ParseAttachments( _ParseAttachments(
message: message, 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( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: _getBackgroundColor(context), color: _getBackgroundColor(context),
@@ -229,193 +236,106 @@ class _ParseAttachments extends StatelessWidget {
final Message message; final Message message;
final StreamMessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
final Map<String, QuotedMessageAttachmentThumbnailBuilder>? final _Builders? attachmentThumbnailBuilders;
attachmentThumbnailBuilders;
bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child; final attachment = message.attachments.first;
Attachment attachment;
if (_containsLinkAttachment) { var attachmentBuilders = attachmentThumbnailBuilders;
attachment = message.attachments.firstWhere( attachmentBuilders ??= _createDefaultAttachmentBuilders();
(element) => element.ogScrapeUrl != null || element.titleLink != null,
// 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'; return Container(
final isVideoFile = attachment.title?.mimeType?.type == 'video'; key: Key(attachment.id),
clipBehavior: clipBehavior,
decoration: decoration,
constraints: const BoxConstraints.tightFor(width: 36, height: 36),
child: AbsorbPointer(child: attachmentWidget),
);
}
return Material( _Builders _createDefaultAttachmentBuilders() {
clipBehavior: Clip.hardEdge, Widget _createMediaThumbnail(BuildContext context, Attachment media) {
type: MaterialType.transparency, return StreamImageAttachmentThumbnail(
shape: attachment.type == 'file' && (!isImageFile && !isVideoFile) image: media,
? null width: double.infinity,
: RoundedRectangleBorder( height: double.infinity,
side: const BorderSide(width: 0, color: Colors.transparent), 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), 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 { return {
const _VideoAttachmentThumbnail({ AttachmentType.image: _createMediaThumbnail,
required this.attachment, AttachmentType.giphy: _createMediaThumbnail,
}); AttachmentType.video: _createMediaThumbnail,
AttachmentType.urlPreview: _createUrlThumbnail,
final Attachment attachment; AttachmentType.file: _createFileThumbnail,
};
@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(),
);
} }
} }
@@ -1169,7 +1169,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
} }
final containsUrl = quotedMessage.attachments.any((it) { final containsUrl = quotedMessage.attachments.any((it) {
return it.titleLink != null; return it.type == AttachmentType.urlPreview;
}); });
return StreamQuotedMessageWidget( return StreamQuotedMessageWidget(
@@ -1316,8 +1316,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
message = message.copyWith(text: '/${message.command} ${message.text}'); message = message.copyWith(text: '/${message.command} ${message.text}');
} }
final skipEnrichUrl = _effectiveController.ogAttachment == null;
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled; shouldKeepFocus ??= !_commandEnabled;
@@ -1341,10 +1339,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame;
} }
await _sendOrUpdateMessage( await _sendOrUpdateMessage(message: message);
message: message,
skipEnrichUrl: skipEnrichUrl,
);
if (mounted) { if (mounted) {
if (shouldKeepFocus) { if (shouldKeepFocus) {
@@ -1357,36 +1352,29 @@ class StreamMessageInputState extends State<StreamMessageInput>
Future<void> _sendOrUpdateMessage({ Future<void> _sendOrUpdateMessage({
required Message message, required Message message,
bool skipEnrichUrl = false,
}) async { }) async {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
try { try {
Future sendingFuture; Future sendingFuture;
if (_isEditing) { if (_isEditing) {
sendingFuture = channel.updateMessage( sendingFuture = channel.updateMessage(message);
message,
skipEnrichUrl: skipEnrichUrl,
);
} else { } else {
sendingFuture = channel.sendMessage( sendingFuture = channel.sendMessage(message);
message,
skipEnrichUrl: skipEnrichUrl,
);
} }
final resp = await sendingFuture; final resp = await sendingFuture;
if (resp.message?.type == 'error') { if (resp.message?.isError ?? false) {
_effectiveController.message = message; _effectiveController.message = message;
} }
_startSlowMode(); _startSlowMode();
widget.onMessageSent?.call(resp.message); widget.onMessageSent?.call(resp.message);
} catch (e, stk) { } catch (e, stk) {
if (widget.onError != null) { if (widget.onError != null) {
widget.onError?.call(e, stk); return widget.onError?.call(e, stk);
} else {
rethrow;
} }
rethrow;
} }
} }
@@ -1,14 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.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/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 '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. /// WidgetBuilder used to build the message input attachment list.
/// ///
/// see more: /// see more:
@@ -93,7 +90,7 @@ class _StreamMessageInputAttachmentListState
// Split the attachments into file and media attachments. // Split the attachments into file and media attachments.
for (final attachment in widget.attachments) { for (final attachment in widget.attachments) {
if (attachment.type == 'file') { if (attachment.type == AttachmentType.file) {
fileAttachments.add(attachment); fileAttachments.add(attachment);
} else { } else {
mediaAttachments.add(attachment); mediaAttachments.add(attachment);
@@ -123,7 +120,7 @@ class _StreamMessageInputAttachmentListState
} }
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 6),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@@ -203,23 +200,19 @@ class MessageInputFileAttachments extends StatelessWidget {
} }
// Otherwise, use the default builder. // Otherwise, use the default builder.
return ClipRRect( return StreamFileAttachment(
key: Key(attachment.id), message: Message(), // Dummy message
borderRadius: BorderRadius.circular(10), file: attachment,
child: StreamFileAttachment( constraints: BoxConstraints.loose(Size(
message: Message(), // dummy message MediaQuery.of(context).size.width * 0.65,
file: attachment, 56,
constraints: BoxConstraints.loose(Size( )),
MediaQuery.of(context).size.width * 0.65, trailing: Padding(
56, padding: const EdgeInsets.all(8),
)), child: RemoveAttachmentButton(
trailing: Padding( onPressed: onRemovePressed != null
padding: const EdgeInsets.all(8), ? () => onRemovePressed!(attachment)
child: RemoveAttachmentButton( : null,
onPressed: onRemovePressed != null
? () => onRemovePressed!(attachment)
: null,
),
), ),
), ),
); );
@@ -258,7 +251,8 @@ class MessageInputMediaAttachments extends StatelessWidget {
height: 104, height: 104,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, 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>( children: attachments.map<Widget>(
(attachment) { (attachment) {
// If a custom builder is provided, use it. // If a custom builder is provided, use it.
@@ -267,27 +261,47 @@ class MessageInputMediaAttachments extends StatelessWidget {
return builder(context, attachment, onRemovePressed); 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), key: Key(attachment.id),
borderRadius: BorderRadius.circular(10), clipBehavior: Clip.hardEdge,
child: Stack( decoration: ShapeDecoration(shape: shape),
children: <Widget>[ child: AspectRatio(
AspectRatio( aspectRatio: 1,
aspectRatio: 1, child: Stack(
child: MessageInputMediaAttachmentThumbnail( alignment: Alignment.center,
attachment: attachment, children: <Widget>[
StreamMediaAttachmentThumbnail(
media: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
), ),
), if (attachment.type == AttachmentType.video)
Positioned( Positioned(
top: 8, left: 8,
right: 8, bottom: 8,
child: RemoveAttachmentButton( child: StreamSvgIcon.videoCall(),
onPressed: onRemovePressed != null ),
? () => onRemovePressed!(attachment) Positioned(
: null, 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. /// Material Button used for removing attachments.
class RemoveAttachmentButton extends StatelessWidget { class RemoveAttachmentButton extends StatelessWidget {
/// Creates a new remove attachment button. /// Creates a new remove attachment button.
@@ -104,6 +104,7 @@ class StreamMessageListView extends StatefulWidget {
this.loadingBuilder, this.loadingBuilder,
this.emptyBuilder, this.emptyBuilder,
this.systemMessageBuilder, this.systemMessageBuilder,
this.ephemeralMessageBuilder,
this.messageListBuilder, this.messageListBuilder,
this.errorBuilder, this.errorBuilder,
this.messageFilter, this.messageFilter,
@@ -149,6 +150,9 @@ class StreamMessageListView extends StatefulWidget {
/// {@macro systemMessageBuilder} /// {@macro systemMessageBuilder}
final SystemMessageBuilder? systemMessageBuilder; final SystemMessageBuilder? systemMessageBuilder;
/// {@macro ephemeralMessageBuilder}
final EphemeralMessageBuilder? ephemeralMessageBuilder;
/// {@macro parentMessageBuilder} /// {@macro parentMessageBuilder}
final ParentMessageBuilder? parentMessageBuilder; final ParentMessageBuilder? parentMessageBuilder;
@@ -915,13 +919,19 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
final hasFileAttachment =
message.attachments.any((it) => it.type == AttachmentType.file);
final hasUrlAttachment = 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 = final borderSide = isOnlyEmoji ? BorderSide.none : null;
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final defaultMessageWidget = StreamMessageWidget( final defaultMessageWidget = StreamMessageWidget(
showReplyMessage: false, showReplyMessage: false,
@@ -935,13 +945,34 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
showUsername: !isMyMessage, showUsername: !isMyMessage,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
showSendingIndicator: false, 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( borderRadiusGeometry: BorderRadius.only(
topLeft: const Radius.circular(16), topLeft: const Radius.circular(16),
bottomLeft: bottomLeft: isMyMessage ? const Radius.circular(16) : Radius.zero,
isMyMessage ? const Radius.circular(16) : const Radius.circular(2),
topRight: const Radius.circular(16), topRight: const Radius.circular(16),
bottomRight: bottomRight: isMyMessage ? Radius.zero : const Radius.circular(16),
isMyMessage ? const Radius.circular(2) : const Radius.circular(16),
), ),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8, vertical: 8,
@@ -1061,14 +1092,8 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
} }
if (message.isEphemeral) { if (message.isEphemeral) {
// return widget.ephemeralMessageBuilder?.call(context, message) ?? return widget.ephemeralMessageBuilder?.call(context, message) ??
return StreamEphemeralMessage( StreamEphemeralMessage(message: message);
message: message,
// onMessageTap: (message) {
// widget.onEphemeralMessageTap?.call(message);
// FocusScope.of(context).unfocus();
// },
);
} }
final userId = StreamChat.of(context).currentUser!.id; final userId = StreamChat.of(context).currentUser!.id;
@@ -1088,14 +1113,21 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
} }
final hasFileAttachment = 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 = final isThreadMessage =
message.parentId != null && message.showInChannel == true; message.parentId != null && message.showInChannel == true;
final hasReplies = message.replyCount! > 0; 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) && final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
!hasReplies && !hasReplies &&
@@ -1119,13 +1151,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final isEphemeral = message.isEphemeral; final borderSide = isOnlyEmoji ? BorderSide.none : null;
final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide =
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
final currentUser = StreamChat.of(context).currentUser; final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? []; final members = StreamChannel.of(context).channel.state?.members ?? [];
@@ -1170,27 +1196,39 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
showFlagButton: !isMyMessage, showFlagButton: !isMyMessage,
borderSide: borderSide, borderSide: borderSide,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
attachmentBorderRadiusGeometry: BorderRadius.only( attachmentShape: RoundedRectangleBorder(
topLeft: Radius.circular(attachmentBorderRadius), side: BorderSide(
bottomLeft: isMyMessage color: _streamTheme.colorTheme.borders,
? Radius.circular(attachmentBorderRadius) strokeAlign: BorderSide.strokeAlignOutside,
: Radius.circular( ),
(hasTimeDiff || !isNextUserSame) && borderRadius: BorderRadius.only(
!(hasReplies || isThreadMessage || hasFileAttachment) topLeft: Radius.circular(attachmentBorderRadius),
? 0 bottomLeft: isMyMessage
: attachmentBorderRadius, ? Radius.circular(attachmentBorderRadius)
), : Radius.circular(
topRight: Radius.circular(attachmentBorderRadius), (hasTimeDiff || !isNextUserSame) &&
bottomRight: isMyMessage !(hasReplies || isThreadMessage || hasFileAttachment)
? Radius.circular( ? 0
(hasTimeDiff || !isNextUserSame) && : attachmentBorderRadius,
!(hasReplies || isThreadMessage || hasFileAttachment) ),
? 0 topRight: Radius.circular(attachmentBorderRadius),
: attachmentBorderRadius, bottomRight: isMyMessage
) ? Radius.circular(
: Radius.circular(attachmentBorderRadius), (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( borderRadiusGeometry: BorderRadius.only(
topLeft: const Radius.circular(16), topLeft: const Radius.circular(16),
bottomLeft: isMyMessage bottomLeft: isMyMessage
@@ -22,6 +22,7 @@ class MessageCard extends StatefulWidget {
required this.isGiphy, required this.isGiphy,
required this.attachmentBuilders, required this.attachmentBuilders,
required this.attachmentPadding, required this.attachmentPadding,
required this.attachmentShape,
required this.onAttachmentTap, required this.onAttachmentTap,
required this.onShowMessage, required this.onShowMessage,
required this.onReplyTap, required this.onReplyTap,
@@ -80,6 +81,9 @@ class MessageCard extends StatefulWidget {
/// {@macro attachmentPadding} /// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding; final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap} /// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap; final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -200,6 +204,7 @@ class _MessageCardState extends State<MessageCard> {
message: widget.message, message: widget.message,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding, attachmentPadding: widget.attachmentPadding,
attachmentShape: widget.attachmentShape,
onAttachmentTap: widget.onAttachmentTap, onAttachmentTap: widget.onAttachmentTap,
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
@@ -49,11 +49,9 @@ class StreamMessageWidget extends StatefulWidget {
this.reverse = false, this.reverse = false,
this.translateUserAvatar = true, this.translateUserAvatar = true,
this.shape, this.shape,
this.attachmentShape,
this.borderSide, this.borderSide,
this.attachmentBorderSide,
this.borderRadiusGeometry, this.borderRadiusGeometry,
this.attachmentBorderRadiusGeometry, this.attachmentShape,
this.onMentionTap, this.onMentionTap,
this.onMessageTap, this.onMessageTap,
this.showReactionPicker = true, this.showReactionPicker = true,
@@ -334,21 +332,11 @@ class StreamMessageWidget extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final BorderSide? borderSide; final BorderSide? borderSide;
/// {@template attachmentBorderSide}
/// The borderSide of an attachment
/// {@endtemplate}
final BorderSide? attachmentBorderSide;
/// {@template borderRadiusGeometry} /// {@template borderRadiusGeometry}
/// The border radius of the message text /// The border radius of the message text
/// {@endtemplate} /// {@endtemplate}
final BorderRadiusGeometry? borderRadiusGeometry; final BorderRadiusGeometry? borderRadiusGeometry;
/// {@template attachmentBorderRadiusGeometry}
/// The border radius of an attachment
/// {@endtemplate}
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
/// {@template padding} /// {@template padding}
/// The padding of the widget /// The padding of the widget
/// {@endtemplate} /// {@endtemplate}
@@ -542,9 +530,7 @@ class StreamMessageWidget extends StatefulWidget {
ShapeBorder? shape, ShapeBorder? shape,
ShapeBorder? attachmentShape, ShapeBorder? attachmentShape,
BorderSide? borderSide, BorderSide? borderSide,
BorderSide? attachmentBorderSide,
BorderRadiusGeometry? borderRadiusGeometry, BorderRadiusGeometry? borderRadiusGeometry,
BorderRadiusGeometry? attachmentBorderRadiusGeometry,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
EdgeInsets? textPadding, EdgeInsets? textPadding,
EdgeInsetsGeometry? attachmentPadding, EdgeInsetsGeometry? attachmentPadding,
@@ -603,10 +589,7 @@ class StreamMessageWidget extends StatefulWidget {
shape: shape ?? this.shape, shape: shape ?? this.shape,
attachmentShape: attachmentShape ?? this.attachmentShape, attachmentShape: attachmentShape ?? this.attachmentShape,
borderSide: borderSide ?? this.borderSide, borderSide: borderSide ?? this.borderSide,
attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide,
borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry, borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry,
attachmentBorderRadiusGeometry:
attachmentBorderRadiusGeometry ?? this.attachmentBorderRadiusGeometry,
padding: padding ?? this.padding, padding: padding ?? this.padding,
textPadding: textPadding ?? this.textPadding, textPadding: textPadding ?? this.textPadding,
attachmentPadding: attachmentPadding ?? this.attachmentPadding, attachmentPadding: attachmentPadding ?? this.attachmentPadding,
@@ -691,8 +674,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
/// {@template isGiphy} /// {@template isGiphy}
/// `true` if any of the [message]'s attachments are a giphy. /// `true` if any of the [message]'s attachments are a giphy.
/// {@endtemplate} /// {@endtemplate}
bool get isGiphy => bool get isGiphy => widget.message.attachments
widget.message.attachments.any((element) => element.type == 'giphy'); .any((element) => element.type == AttachmentType.giphy);
/// {@template isOnlyEmoji} /// {@template isOnlyEmoji}
/// `true` if [message.text] contains only emoji. /// `true` if [message.text] contains only emoji.
@@ -749,7 +732,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
bool get shouldShowEditAction => bool get shouldShowEditAction =>
widget.showEditMessage && widget.showEditMessage &&
!isDeleteFailed && !isDeleteFailed &&
!widget.message.attachments.any((element) => element.type == 'giphy'); !widget.message.attachments
.any((element) => element.type == AttachmentType.giphy);
bool get shouldShowResendAction => bool get shouldShowResendAction =>
widget.showResendMessage && (isSendFailed || isUpdateFailed); widget.showResendMessage && (isSendFailed || isUpdateFailed);
@@ -762,7 +746,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
bool get shouldShowEditMessage => bool get shouldShowEditMessage =>
widget.showEditMessage && widget.showEditMessage &&
!isDeleteFailed && !isDeleteFailed &&
!widget.message.attachments.any((element) => element.type == 'giphy'); !widget.message.attachments
.any((element) => element.type == AttachmentType.giphy);
bool get shouldShowThreadReplyAction => bool get shouldShowThreadReplyAction =>
widget.showThreadReplyMessage && widget.showThreadReplyMessage &&
@@ -853,6 +838,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
textPadding: widget.textPadding, textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders, attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding, attachmentPadding: widget.attachmentPadding,
attachmentShape: widget.attachmentShape,
onAttachmentTap: widget.onAttachmentTap, onAttachmentTap: widget.onAttachmentTap,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
@@ -48,6 +48,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.isGiphy, required this.isGiphy,
required this.attachmentBuilders, required this.attachmentBuilders,
required this.attachmentPadding, required this.attachmentPadding,
required this.attachmentShape,
required this.onAttachmentTap, required this.onAttachmentTap,
required this.onShowMessage, required this.onShowMessage,
required this.onReplyTap, required this.onReplyTap,
@@ -150,6 +151,9 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro attachmentPadding} /// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding; final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap} /// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap; final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -341,6 +345,7 @@ class MessageWidgetContent extends StatelessWidget {
isGiphy: isGiphy, isGiphy: isGiphy,
attachmentBuilders: attachmentBuilders, attachmentBuilders: attachmentBuilders,
attachmentPadding: attachmentPadding, attachmentPadding: attachmentPadding,
attachmentShape: attachmentShape,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
onReplyTap: onReplyTap, onReplyTap: onReplyTap,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
@@ -16,6 +16,7 @@ class ParseAttachments extends StatelessWidget {
required this.message, required this.message,
required this.attachmentBuilders, required this.attachmentBuilders,
required this.attachmentPadding, required this.attachmentPadding,
this.attachmentShape,
this.onAttachmentTap, this.onAttachmentTap,
this.onShowMessage, this.onShowMessage,
this.onReplyTap, this.onReplyTap,
@@ -31,6 +32,9 @@ class ParseAttachments extends StatelessWidget {
/// {@macro attachmentPadding} /// {@macro attachmentPadding}
final EdgeInsetsGeometry attachmentPadding; final EdgeInsetsGeometry attachmentPadding;
/// {@macro attachmentShape}
final ShapeBorder? attachmentShape;
/// {@macro onAttachmentTap} /// {@macro onAttachmentTap}
final StreamAttachmentWidgetTapCallback? onAttachmentTap; final StreamAttachmentWidgetTapCallback? onAttachmentTap;
@@ -104,6 +108,8 @@ class ParseAttachments extends StatelessWidget {
var builders = attachmentBuilders; var builders = attachmentBuilders;
builders ??= StreamAttachmentWidgetBuilder.defaultBuilders( builders ??= StreamAttachmentWidgetBuilder.defaultBuilders(
message: message, message: message,
shape: attachmentShape,
padding: attachmentPadding,
onAttachmentTap: onAttachmentTap, onAttachmentTap: onAttachmentTap,
); );
@@ -50,45 +50,47 @@ class StreamMessageReactionsModal extends StatelessWidget {
final child = Center( final child = Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: SafeArea(
padding: const EdgeInsets.all(8), child: Padding(
child: Column( padding: const EdgeInsets.all(8),
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.stretch,
if (showReactionPicker && hasReactionPermission) children: <Widget>[
LayoutBuilder( if (showReactionPicker && hasReactionPermission)
builder: (context, constraints) { LayoutBuilder(
return Align( builder: (context, constraints) {
alignment: Alignment( return Align(
calculateReactionsHorizontalAlignment( alignment: Alignment(
user, calculateReactionsHorizontalAlignment(
message, user,
constraints, message,
fontSize, constraints,
orientation, fontSize,
orientation,
),
0,
), ),
0, child: StreamReactionPicker(
), message: message,
child: StreamReactionPicker( ),
message: message, );
), },
); ),
}, const SizedBox(height: 10),
), IgnorePointer(
const SizedBox(height: 10), child: messageWidget,
IgnorePointer(
child: messageWidget,
),
if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8),
ReactionsCard(
currentUser: user!,
message: message,
messageTheme: messageTheme,
), ),
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message.text?.trim().isEmpty ?? false) return const Offstage(); if (message.text?.trim().isEmpty ?? true) return const Offstage();
return Padding( return Padding(
padding: isOnlyEmoji ? EdgeInsets.zero : textPadding, padding: isOnlyEmoji ? EdgeInsets.zero : textPadding,
child: textBuilder != null child: textBuilder != null
@@ -228,8 +228,7 @@ class StreamChannelListTile extends StatelessWidget {
} }
final hasNonUrlAttachments = lastMessage.attachments final hasNonUrlAttachments = lastMessage.attachments
.where((it) => it.titleLink == null || it.type == 'giphy') .any((it) => it.type != AttachmentType.urlPreview);
.isNotEmpty;
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 4),
@@ -201,6 +201,7 @@ class StreamChatThemeData {
urlAttachmentTitleStyle: textTheme.footnoteBold, urlAttachmentTitleStyle: textTheme.footnoteBold,
urlAttachmentTextStyle: textTheme.footnote, urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTitleMaxLine: 1, urlAttachmentTitleMaxLine: 1,
urlAttachmentTextMaxLine: 3,
), ),
otherMessageTheme: StreamMessageThemeData( otherMessageTheme: StreamMessageThemeData(
reactionsBackgroundColor: colorTheme.borders, reactionsBackgroundColor: colorTheme.borders,
@@ -227,6 +228,7 @@ class StreamChatThemeData {
urlAttachmentTitleStyle: textTheme.footnoteBold, urlAttachmentTitleStyle: textTheme.footnoteBold,
urlAttachmentTextStyle: textTheme.footnote, urlAttachmentTextStyle: textTheme.footnote,
urlAttachmentTitleMaxLine: 1, urlAttachmentTitleMaxLine: 1,
urlAttachmentTextMaxLine: 3,
), ),
messageInputTheme: StreamMessageInputThemeData( messageInputTheme: StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:diacritic/diacritic.dart'; import 'package:diacritic/diacritic.dart';
@@ -5,6 +6,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.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/src/localization/translations.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -114,7 +117,7 @@ extension PlatformFileX on PlatformFile {
final file = toAttachmentFile; final file = toAttachmentFile;
final extraDataMap = <String, Object>{}; final extraDataMap = <String, Object>{};
final mimeType = file.mimeType?.mimeType; final mimeType = file.mediaType?.mimeType;
if (mimeType != null) { if (mimeType != null) {
extraDataMap['mime_type'] = mimeType; extraDataMap['mime_type'] = mimeType;
@@ -151,7 +154,7 @@ extension XFileX on XFile {
final extraDataMap = <String, Object>{}; final extraDataMap = <String, Object>{};
final mimeType = this.mimeType ?? file.mimeType?.mimeType; final mimeType = this.mimeType ?? file.mediaType?.mimeType;
if (mimeType != null) { if (mimeType != null) {
extraDataMap['mime_type'] = mimeType; extraDataMap['mime_type'] = mimeType;
@@ -367,7 +370,7 @@ extension MessageX on Message {
/// Returns an approximation of message size /// Returns an approximation of message size
double roughMessageSize(double? fontSize) { double roughMessageSize(double? fontSize) {
var messageTextLength = min(text!.biggestLine().length, 65); var messageTextLength = min(text?.biggestLine().length ?? 0, 65);
if (quotedMessage != null) { if (quotedMessage != null) {
var quotedMessageLength = 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, Message,
); );
/// {@template ephemeralMessageBuilder}
/// A widget builder for creating custom ephemeral messages.
/// {@endtemplate}
typedef EphemeralMessageBuilder = Widget Function(
BuildContext,
Message,
);
/// {@template threadBuilder} /// {@template threadBuilder}
/// A widget builder for creating custom thread UI. /// A widget builder for creating custom thread UI.
/// {@endtemplate} /// {@endtemplate}
@@ -142,6 +142,8 @@ class StreamVideoThumbnailImage
int get hashCode => Object.hash(video, scale); int get hashCode => Object.hash(video, scale);
@override @override
String toString() => String toString() {
'${objectRuntimeType(this, 'StreamVideoThumbnailImage')}($video, scale: $scale)'; 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.dart';
export 'src/attachment/attachment_title.dart'; export 'src/attachment/attachment_title.dart';
export 'src/attachment/gallery_attachment.dart';
export 'src/attachment/handler/stream_attachment_handler.dart'; export 'src/attachment/handler/stream_attachment_handler.dart';
export 'src/attachment/image_attachment.dart'; export 'src/attachment/image_attachment.dart';
export 'src/attachment/gallery_attachment.dart';
export 'src/attachment/stream_attachment_package.dart'; export 'src/attachment/stream_attachment_package.dart';
export 'src/attachment/url_attachment.dart'; export 'src/attachment/url_attachment.dart';
export 'src/attachment/video_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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.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 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../mocks.dart'; import '../mocks.dart';
@@ -18,6 +19,27 @@ void main() {
final themeData = ThemeData(); final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: StreamChatTheme( home: StreamChatTheme(
@@ -31,26 +53,17 @@ void main() {
300, 300,
)), )),
message: Message(), message: Message(),
attachments: [ attachments: attachments,
Attachment( itemBuilder: (context, index) {
type: 'image', final attachment = attachments[index];
title: 'example.png',
imageUrl: return StreamImageAttachmentThumbnail(
'https://logowik.com/content/uploads/images/flutter5786.jpg', image: attachment,
extraData: const { width: double.infinity,
'mime_type': 'png', height: double.infinity,
}, fit: BoxFit.cover,
), );
Attachment( },
type: 'image',
title: 'example.png',
imageUrl:
'https://logowik.com/content/uploads/images/flutter5786.jpg',
extraData: const {
'mime_type': 'png',
},
),
],
), ),
), ),
), ),
@@ -30,7 +30,7 @@ void main() {
300, 300,
)), )),
message: Message(), message: Message(),
file: Attachment( giphy: Attachment(
type: 'giphy', type: 'giphy',
title: 'example.gif', title: 'example.gif',
imageUrl: imageUrl:
@@ -31,7 +31,7 @@ void main() {
300, 300,
)), )),
message: Message(), message: Message(),
file: Attachment( image: Attachment(
type: 'image', type: 'image',
title: 'example.png', title: 'example.png',
imageUrl: imageUrl:
@@ -26,6 +26,7 @@ void main() {
child: SizedBox( child: SizedBox(
child: StreamUrlAttachment( child: StreamUrlAttachment(
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
message: Message(),
hostDisplayName: 'Test', hostDisplayName: 'Test',
urlAttachment: Attachment( urlAttachment: Attachment(
title: 'Flutter', 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, width: 40,
), ),
), ),
color: const Color(0xff101418), color: const Color(0xff111417),
titleStyle: const TextStyle( titleStyle: const TextStyle(
color: Color(0xffffffff), color: Color(0xffffffff),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -73,7 +73,7 @@ final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData(
width: 40, width: 40,
), ),
), ),
color: const Color(0xff87898b), color: const Color(0xff88898a),
titleStyle: const TextStyle( titleStyle: const TextStyle(
color: Color(0xff7f7f7f), color: Color(0xff7f7f7f),
fontSize: 16, fontSize: 16,
@@ -73,11 +73,7 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
return Scaffold( return const SizedBox.shrink();
appBar: StreamGalleryFooter(
mediaAttachmentPackages: Message().getAttachmentPackageList(),
),
);
}, },
), ),
), ),
@@ -116,11 +112,7 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
return Scaffold( return const SizedBox.shrink();
appBar: StreamGalleryFooter(
mediaAttachmentPackages: Message().getAttachmentPackageList(),
),
);
}, },
), ),
), ),
@@ -160,7 +152,7 @@ final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData(
// Mid-lerp theme control // Mid-lerp theme control
const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData( const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
backgroundColor: Color(0xff87898b), backgroundColor: Color(0xff88898a),
shareIconColor: Color(0xff7f7f7f), shareIconColor: Color(0xff7f7f7f),
titleTextStyle: TextStyle( titleTextStyle: TextStyle(
color: Color(0xff7f7f7f), color: Color(0xff7f7f7f),
@@ -169,7 +161,7 @@ const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
), ),
gridIconButtonColor: Color(0xff7f7f7f), gridIconButtonColor: Color(0xff7f7f7f),
bottomSheetBarrierColor: Color(0x4c000000), bottomSheetBarrierColor: Color(0x4c000000),
bottomSheetBackgroundColor: Color(0xff87898b), bottomSheetBackgroundColor: Color(0xff88898a),
bottomSheetPhotosTextStyle: TextStyle( bottomSheetPhotosTextStyle: TextStyle(
color: Color(0xff7f7f7f), color: Color(0xff7f7f7f),
fontSize: 16, fontSize: 16,
@@ -66,22 +66,7 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
final attachment = Attachment( return const SizedBox.shrink();
type: 'video',
title: 'video.mp4',
);
final _message = Message(
createdAt: DateTime.now(),
attachments: [
attachment,
],
);
return Scaffold(
appBar: StreamGalleryHeader(
message: _message,
attachment: _message.attachments[0],
),
);
}, },
), ),
), ),
@@ -116,22 +101,7 @@ void main() {
home: Builder( home: Builder(
builder: (context) { builder: (context) {
_context = context; _context = context;
final attachment = Attachment( return const SizedBox.shrink();
type: 'video',
title: 'video.mp4',
);
final _message = Message(
createdAt: DateTime.now(),
attachments: [
attachment,
],
);
return Scaffold(
appBar: StreamGalleryHeader(
message: _message,
attachment: _message.attachments[0],
),
);
}, },
), ),
), ),
@@ -175,7 +145,7 @@ final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData(
// Light theme test control. // Light theme test control.
final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData( final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
closeButtonColor: const Color(0xff7f7f7f), closeButtonColor: const Color(0xff7f7f7f),
backgroundColor: const Color(0xff87898b), backgroundColor: const Color(0xff88898a),
iconMenuPointColor: const Color(0xff7f7f7f), iconMenuPointColor: const Color(0xff7f7f7f),
titleTextStyle: const TextStyle( titleTextStyle: const TextStyle(
fontSize: 16, fontSize: 16,
@@ -194,7 +164,7 @@ final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
// Dark theme test control. // Dark theme test control.
final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData( final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData(
closeButtonColor: const Color(0xffffffff), closeButtonColor: const Color(0xffffffff),
backgroundColor: const Color(0xff101418), backgroundColor: const Color(0xff121416),
iconMenuPointColor: const Color(0xffffffff), iconMenuPointColor: const Color(0xffffffff),
titleTextStyle: const TextStyle( titleTextStyle: const TextStyle(
fontSize: 16, fontSize: 16,
@@ -68,7 +68,7 @@ final _messageInputThemeControl = StreamMessageInputThemeData(
final _messageInputThemeControlMidLerp = StreamMessageInputThemeData( final _messageInputThemeControlMidLerp = StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300), sendAnimationDuration: const Duration(milliseconds: 300),
inputBackgroundColor: const Color(0xff87898b), inputBackgroundColor: const Color(0xff88898a),
actionButtonColor: const Color(0xff196eff), actionButtonColor: const Color(0xff196eff),
actionButtonIdleColor: const Color(0xff7a7a7a), actionButtonIdleColor: const Color(0xff7a7a7a),
sendButtonColor: const Color(0xff196eff), sendButtonColor: const Color(0xff196eff),
@@ -68,12 +68,7 @@ void main() {
home: Builder( home: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
_context = context; _context = context;
return Scaffold( return const SizedBox.shrink();
body: StreamChannel(
channel: MockChannel(),
child: const StreamMessageListView(),
),
);
}, },
), ),
), ),
@@ -98,12 +93,7 @@ void main() {
home: Builder( home: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
_context = context; _context = context;
return Scaffold( return const SizedBox.shrink();
body: StreamChannel(
channel: MockChannel(),
child: const StreamMessageListView(),
),
);
}, },
), ),
), ),
@@ -151,7 +141,7 @@ final _messageListViewThemeDataControl = StreamMessageListViewThemeData(
); );
const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData(
backgroundColor: Color(0xff87898b), backgroundColor: Color(0xff88898a),
); );
final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData( final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData(