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
@@ -57,6 +57,8 @@ class AttachmentWidgetCatalog {
extension on List<Attachment> {
/// Groups the attachments by their type.
Map<String, List<Attachment>> get grouped {
return groupBy(this, (attachment) => attachment.type!);
return groupBy(where((it) {
return it.type != null;
}), (attachment) => attachment.type!);
}
}
@@ -1,20 +1,13 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/file_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
import 'package:stream_chat_flutter/src/attachment/gallery_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/giphy_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/image_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/url_attachment.dart';
import 'package:stream_chat_flutter/src/attachment/video_attachment.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../attachment_upload_state_builder.dart';
part 'fallback_attachment_builder.dart';
part 'file_attachment_builder.dart';
@@ -82,41 +75,58 @@ abstract class StreamAttachmentWidgetBuilder {
/// widget.
static List<StreamAttachmentWidgetBuilder> defaultBuilders({
required Message message,
ShapeBorder? shape,
EdgeInsetsGeometry padding = const EdgeInsets.all(4),
StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) {
return [
// Handles a mix of image, gif, video, and file attachments.
// Handles a mix of image, gif, video, url and file attachments.
MixedAttachmentBuilder(
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles a mix of image, gif, and video attachments.
GalleryAttachmentBuilder(
shape: shape,
padding: padding,
runSpacing: padding.vertical / 2,
spacing: padding.horizontal / 2,
onAttachmentTap: onAttachmentTap,
),
// Handles file attachments.
FileAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles giphy attachments.
GiphyAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles image attachments.
ImageAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// Handles video attachments.
VideoAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
// We don't handle URL attachments if the message is a reply.
if (message.quotedMessage == null)
UrlAttachmentBuilder(
shape: shape,
padding: padding,
onAttachmentTap: onAttachmentTap,
),
@@ -93,16 +93,6 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
attachments: galleryAttachments,
itemBuilder: (context, index) {
final attachment = galleryAttachments[index];
final attachmentType = attachment.type;
final isImage = attachmentType == AttachmentType.image;
final isVideo = attachmentType == AttachmentType.video;
final isGiphy = attachmentType == AttachmentType.giphy;
assert(
isImage || isVideo || isGiphy,
'Attachment type should be image, video or giphy',
);
VoidCallback? onTap;
if (onAttachmentTap != null) {
@@ -112,29 +102,13 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
return InkWell(
onTap: onTap,
child: Stack(
alignment: Alignment.center,
children: [
if (isImage)
StreamImageAttachmentThumbnail(
image: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
)
else if (isVideo)
StreamVideoAttachmentThumbnail(
video: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
)
else if (isGiphy)
StreamGiphyAttachmentThumbnail(
giphy: attachment,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
StreamMediaAttachmentThumbnail(
media: attachment,
width: constraints.maxWidth,
height: constraints.maxHeight,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
@@ -62,6 +62,7 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: InkWell(
onTap: onTap,
child: StreamImageAttachment(
shape: shape,
message: message,
constraints: constraints,
image: image,
@@ -3,67 +3,71 @@ part of 'attachment_widget_builder.dart';
/// {@template mixedAttachmentBuilder}
/// A widget builder for Mixed attachment type.
///
/// This builder is used when a message contains both image/video/giphy and file
/// attachments.
/// This builder is used when a message contains a mix of media type and file
/// or url preview attachments.
///
/// This builder will render first image/video/giphy attachment and then render
/// the file attachments.
/// This builder will render first the url preview or file attachment and then
/// the media attachments.
/// {@endtemplate}
class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
/// {@macro mixedAttachmentBuilder}
MixedAttachmentBuilder({
this.shape,
this.padding = const EdgeInsets.all(4),
this.onAttachmentTap,
StreamAttachmentWidgetTapCallback? onAttachmentTap,
}) : _imageAttachmentBuilder = ImageAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_videoAttachmentBuilder = VideoAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_giphyAttachmentBuilder = GiphyAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_galleryAttachmentBuilder = GalleryAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
),
_fileAttachmentBuilder = FileAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
),
_urlAttachmentBuilder = UrlAttachmentBuilder(
padding: EdgeInsets.zero,
onAttachmentTap: onAttachmentTap,
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
);
/// The shape of the gallery attachment.
final ShapeBorder? shape;
/// The padding to apply to the gallery attachment widget.
/// The padding to apply to the mixed attachment widget.
final EdgeInsetsGeometry padding;
/// The callback to call when the attachment is tapped.
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder;
late final StreamAttachmentWidgetBuilder _urlAttachmentBuilder;
@override
bool canHandle(
Message message,
Map<String, List<Attachment>> attachments,
) {
final containsImage = attachments.keys.contains(AttachmentType.image);
final containsVideo = attachments.keys.contains(AttachmentType.video);
final containsGiphy = attachments.keys.contains(AttachmentType.giphy);
final containsFile = attachments.keys.contains(AttachmentType.file);
final types = attachments.keys;
final containsImage = types.contains(AttachmentType.image);
final containsVideo = types.contains(AttachmentType.video);
final containsGiphy = types.contains(AttachmentType.giphy);
final containsFile = types.contains(AttachmentType.file);
final containsUrlPreview = types.contains(AttachmentType.urlPreview);
final containsMedia = containsImage || containsVideo || containsGiphy;
return containsMedia && containsFile;
return containsMedia && containsFile ||
containsMedia && containsUrlPreview ||
containsFile && containsUrlPreview ||
containsMedia && containsFile && containsUrlPreview;
}
@override
@@ -74,6 +78,7 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
) {
assert(debugAssertCanHandle(message, attachments), '');
final urls = attachments[AttachmentType.urlPreview];
final files = attachments[AttachmentType.file];
final images = attachments[AttachmentType.image];
final videos = attachments[AttachmentType.video];
@@ -86,11 +91,14 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (urls != null)
_urlAttachmentBuilder.build(context, message, {
AttachmentType.urlPreview: urls,
}),
if (files != null)
for (final file in files)
_fileAttachmentBuilder.build(context, message, {
AttachmentType.file: [file],
}),
_fileAttachmentBuilder.build(context, message, {
AttachmentType.file: files,
}),
if (shouldBuildGallery)
_galleryAttachmentBuilder.build(context, message, {
if (images != null) AttachmentType.image: images,
@@ -128,19 +128,21 @@ class _FileTypeImage extends StatelessWidget {
file: file,
width: double.infinity,
height: double.infinity,
// fit: BoxFit.cover,
);
final mimeType = file.title?.mimeType?.type;
final isImage = mimeType == 'image';
final isVideo = mimeType == 'video';
final mediaType = file.title?.mediaType;
final isImage = mediaType?.type == AttachmentType.image;
final isVideo = mediaType?.type == AttachmentType.video;
if (isImage || isVideo) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
child = Container(
clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(color: colorTheme.borders),
side: BorderSide(
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(8),
),
),
@@ -127,6 +127,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1],
[1],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -145,6 +147,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: const [
[1, 1],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -170,6 +174,8 @@ class StreamGalleryAttachment extends StatelessWidget {
pattern: [
if (isLandscape1) [2, 1] else [1, 2],
],
spacing: spacing,
runSpacing: runSpacing,
children: [
itemBuilder(context, 0),
itemBuilder(context, 1),
@@ -204,6 +210,8 @@ class StreamGalleryAttachment extends StatelessWidget {
[1],
[1, 1],
],
spacing: spacing,
runSpacing: runSpacing,
reverse: !isLandscape1,
children: [
itemBuilder(context, 0),
@@ -238,6 +246,8 @@ class StreamGalleryAttachment extends StatelessWidget {
return FlexGrid(
pattern: pattern,
maxChildren: 4,
spacing: spacing,
runSpacing: runSpacing,
children: children,
overlayBuilder: (context, remaining) {
return IgnorePointer(
@@ -12,7 +12,7 @@ class StreamGiphyAttachment extends StatelessWidget {
super.key,
required this.message,
required this.giphy,
this.type = GiphyInfoType.fixedHeightDownsampled,
this.type = GiphyInfoType.original,
this.shape,
this.constraints = const BoxConstraints(),
});
@@ -50,18 +50,18 @@ Future<AttachmentData> downloadAttachmentData(
String? downloadUrl;
String? fileName;
/* ---IMAGES/GIFS--- */
if (type == 'image') {
if (type == AttachmentType.image) {
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
fileName = attachment.title;
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
}
/* ---GIPHY's--- */
else if (type == 'giphy') {
else if (type == AttachmentType.giphy) {
downloadUrl = attachment.thumbUrl;
fileName = '${attachment.title}.gif';
}
/* ---FILES AND VIDEOS--- */
else if (type == 'file' || type == 'video') {
else if (type == AttachmentType.file || type == AttachmentType.video) {
downloadUrl = attachment.assetUrl;
fileName = attachment.title;
}
@@ -50,9 +50,9 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mimeType = file.title?.mimeType?.type;
final mediaType = file.title?.mediaType;
final isImage = mimeType == 'image';
final isImage = mediaType?.type == AttachmentType.image;
if (isImage) {
return StreamImageAttachmentThumbnail(
image: file,
@@ -62,7 +62,7 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
);
}
final isVideo = mimeType == 'video';
final isVideo = mediaType?.type == AttachmentType.video;
if (isVideo) {
return StreamVideoAttachmentThumbnail(
video: file,
@@ -73,6 +73,6 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
}
// Return a generic file type icon.
return getFileTypeImage(mimeType);
return getFileTypeImage(mediaType?.mimeType);
}
}
@@ -50,6 +50,9 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -2,49 +2,12 @@ import 'dart:io' show File;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:image_size_getter/file_input.dart'; // For compatibility with flutter web.
import 'package:image_size_getter/image_size_getter.dart' hide Size;
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
extension AspectRatioX on Attachment {
/// Returns the size of the attachment if it is an image or giffy.
/// Otherwise, returns null.
Size? get originalSize {
// Return null if the attachment is not an image or giffy.
if (type != 'image' && type != 'giphy') return null;
// Calculate size locally if the attachment is not uploaded yet.
final file = this.file;
if (file != null) {
ImageInput? input;
if (file.bytes != null) {
input = MemoryInput(file.bytes!);
} else if (file.path != null) {
input = FileInput(File(file.path!));
}
// Return null if the file does not contain enough information.
if (input == null) return null;
final size = ImageSizeGetter.getSize(input);
if (size.needRotate) {
return Size(size.height.toDouble(), size.width.toDouble());
}
return Size(size.width.toDouble(), size.height.toDouble());
}
// Otherwise, use the size provided by the server.
final width = originalWidth;
final height = originalHeight;
if (width == null || height == null) return null;
return Size(width.toDouble(), height.toDouble());
}
}
/// {@template imageAttachmentThumbnail}
/// Widget for building image attachment thumbnail.
///
@@ -101,6 +64,9 @@ class StreamImageAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart';
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@template mediaAttachmentThumbnail}
/// Widget for building media attachment thumbnail.
///
/// This widget is used when the [Attachment.type] is [AttachmentType.image],
/// [AttachmentType.video] or [AttachmentType.giphy].
///
/// see also:
/// * [StreamImageAttachmentThumbnail]
/// * [StreamVideoAttachmentThumbnail]
/// * [StreamGiphyAttachmentThumbnail]
/// {@endtemplate}
class StreamMediaAttachmentThumbnail extends StatelessWidget {
/// {@macro mediaAttachmentThumbnail}
const StreamMediaAttachmentThumbnail({
super.key,
required this.media,
this.width,
this.height,
this.fit,
this.thumbnailSize,
this.thumbnailResizeType = 'clip',
this.thumbnailCropType = 'center',
this.gifInfoType = GiphyInfoType.original,
this.errorBuilder = _defaultErrorBuilder,
});
/// The giphy attachment to build the thumbnail for.
final Attachment media;
/// The width of the thumbnail.
final double? width;
/// The height of the thumbnail.
final double? height;
/// How to inscribe the thumbnail into the space allocated during layout.
final BoxFit? fit;
/// Builder used when the thumbnail fails to load.
final ThumbnailErrorBuilder errorBuilder;
/// Size of the attachment image thumbnail.
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final Size? thumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final String /*clip|crop|scale|fill*/ thumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
///
/// Ignored if the [Attachment.type] is not [AttachmentType.image].
final String /*center|top|bottom|left|right*/ thumbnailCropType;
/// The type of giphy thumbnail to build.
///
/// Ignored if the [Attachment.type] is not [AttachmentType.giphy].
final GiphyInfoType gifInfoType;
// Default error builder for image attachment thumbnail.
static Widget _defaultErrorBuilder(
BuildContext context,
Object error,
StackTrace? stackTrace,
) {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@override
Widget build(BuildContext context) {
final type = media.type;
if (type == AttachmentType.image) {
return StreamImageAttachmentThumbnail(
image: media,
width: width,
height: height,
fit: fit,
thumbnailSize: thumbnailSize,
thumbnailResizeType: thumbnailResizeType,
thumbnailCropType: thumbnailCropType,
errorBuilder: errorBuilder,
);
}
if (type == AttachmentType.giphy) {
return StreamGiphyAttachmentThumbnail(
giphy: media,
width: width,
height: height,
fit: fit,
type: gifInfoType,
errorBuilder: errorBuilder,
);
}
if (type == AttachmentType.video) {
return StreamVideoAttachmentThumbnail(
video: media,
width: width,
height: height,
fit: fit,
errorBuilder: errorBuilder,
);
}
return errorBuilder(
context,
'Unsupported attachment type: $type',
StackTrace.current,
);
}
}
@@ -21,8 +21,20 @@ class ThumbnailError extends StatelessWidget {
super.key,
required this.error,
this.stackTrace,
this.width,
this.height,
this.fit,
});
/// The width of the thumbnail.
final double? width;
/// The height of the thumbnail.
final double? height;
/// How to inscribe the thumbnail into the space allocated during layout.
final BoxFit? fit;
/// The error that triggered this error widget.
final Object error;
@@ -33,7 +45,9 @@ class ThumbnailError extends StatelessWidget {
Widget build(BuildContext context) {
return Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
width: width,
height: height,
fit: fit,
package: 'stream_chat_flutter',
);
}
@@ -46,6 +46,9 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget {
return ThumbnailError(
error: error,
stackTrace: stackTrace,
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
);
}
@@ -47,7 +47,7 @@ class StreamUrlAttachment extends StatelessWidget {
color: colorTheme.borders,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(8),
);
final backgroundColor = messageTheme.urlAttachmentBackgroundColor;
@@ -62,44 +62,43 @@ class StreamUrlAttachment extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (urlAttachment.imageUrl != null)
Stack(
children: [
AspectRatio(
// Default aspect ratio for Open Graph images.
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
aspectRatio: 1.91 / 1,
child: StreamImageAttachmentThumbnail(
image: urlAttachment,
fit: BoxFit.cover,
),
Stack(
children: [
AspectRatio(
// Default aspect ratio for Open Graph images.
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
aspectRatio: 1.91 / 1,
child: StreamImageAttachmentThumbnail(
image: urlAttachment,
fit: BoxFit.cover,
),
Positioned(
left: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
color: backgroundColor,
),
Positioned(
left: 0,
bottom: 0,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 12,
bottom: 4,
),
child: Text(
hostDisplayName,
style: messageTheme.urlAttachmentHostStyle,
),
color: backgroundColor,
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 12,
bottom: 4,
),
child: Text(
hostDisplayName,
style: messageTheme.urlAttachmentHostStyle,
),
),
),
],
),
),
],
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(