feat(ui): Added thumbnailSize, thumbnailResizeType, and thumbnailCropType params to StreamMessageWidget and StreamAttachmentPicker to customize the appearance of image thumbnails.

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2022-08-19 12:04:50 +05:30
committed by xsahil03x
parent 55bb828bd1
commit ae199c636a
10 changed files with 453 additions and 194 deletions
+39
View File
@@ -4,6 +4,45 @@
- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions
when network is off or spotty.
- Fixes an error where Stream CDN images were not being resized in the message list view.
🚀 Improved
- Automatically resize images that are above a specific pixel count to ensure resizing works:
getstream.io/chat/docs/go-golang/file_uploads/#image-resizing
✅ Added
- Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params
to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image
thumbnails.
```dart
StreamMessageInput(
focusNode: _focusNode,
messageInputController: _messageInputController,
attachmentsPickerBuilder: (_, __, picker) {
return picker.copyWith(
attachmentThumbnailSize: ...,
attachmentThumbnailFormat: ...,
attachmentThumbnailQuality: ...,
attachmentThumbnailScale: ...,
);
},
),
```
```dart
StreamMessageListView(
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
imageAttachmentThumbnailSize: ...,
imageAttachmentThumbnailCropType: ...,
imageAttachmentThumbnailResizeType: ...,
);
},
),
```
## 4.4.1
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro image_attachment}
@@ -24,6 +25,9 @@ class StreamImageAttachment extends StreamAttachmentWidget {
this.onShowMessage,
this.onReturnAction,
this.onAttachmentTap,
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'crop',
this.imageThumbnailCropType = 'center',
});
/// [StreamMessageThemeData] for showing image title
@@ -41,6 +45,19 @@ class StreamImageAttachment extends StreamAttachmentWidget {
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
/// Size of the attachment image thumbnail.
final Size imageThumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
final String /*clip|crop|scale|fill*/ imageThumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
@override
Widget build(BuildContext context) => source.when(
local: () {
@@ -65,43 +82,22 @@ class StreamImageAttachment extends StreamAttachmentWidget {
var imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
if (imageUrl == null) {
return AttachmentError(size: size);
}
if (imageUrl == null) return AttachmentError(size: size);
var imageUri = Uri.parse(imageUrl);
if (imageUri.host.endsWith('stream-io-cdn.com') &&
(imageUri.queryParameters['h'] == null ||
imageUri.queryParameters['h'] == '*') &&
(imageUri.queryParameters['w'] == null ||
imageUri.queryParameters['w'] == '*') &&
(imageUri.queryParameters['crop'] == null ||
imageUri.queryParameters['crop'] == '*') &&
(imageUri.queryParameters['resize'] == null ||
imageUri.queryParameters['resize'] == '*')) {
imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters,
'h': '400', // TODO: Are these sizes optimal? Consider web/desktop
'w': '400',
'crop': 'center',
'resize': 'clip',
});
} else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) {
imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters,
'height': '400',
'width': '400',
'fit': 'crop',
});
}
imageUrl = imageUri.toString();
imageUrl = imageUrl.getResizedImageUrl(
width: imageThumbnailSize.width,
height: imageThumbnailSize.height,
resize: imageThumbnailResizeType,
crop: imageThumbnailCropType,
);
return _buildImageAttachment(
context,
CachedNetworkImage(
cacheKey: imageUri.replace(queryParameters: {}).toString(),
imageUrl: imageUrl,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
placeholder: (context, __) {
final image = Image.asset(
'images/placeholder.png',
@@ -115,9 +111,7 @@ class StreamImageAttachment extends StreamAttachmentWidget {
child: image,
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) => AttachmentError(size: size),
fit: BoxFit.cover,
),
);
},
@@ -31,6 +31,46 @@ extension StringExtension on String {
/// Levenshtein distance between this and [t].
int levenshteinDistance(String t) => levenshtein(this, t);
/// Returns a resized imageUrl with the given [width], [height], [resize]
/// and [crop] if it is from Stream CDN or Dashboard.
///
/// Read more at https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing
String getResizedImageUrl({
// TODO: Are these sizes optimal? Consider web/desktop
double width = 400,
double height = 400,
String /*clip|crop|scale|fill*/ resize = 'scale',
String /*center|top|bottom|left|right*/ crop = 'center',
}) {
final uri = Uri.parse(this);
final host = uri.host;
final fromStreamCDN = host.endsWith('stream-io-cdn.com');
final fromStreamDashboard = host.endsWith('stream-cloud-uploads.imgix.net');
if (!fromStreamCDN && !fromStreamDashboard) return this;
final queryParameters = {...uri.queryParameters};
if (fromStreamCDN) {
if (queryParameters['h'].isNullOrMatches('*') &&
queryParameters['w'].isNullOrMatches('*') &&
queryParameters['crop'].isNullOrMatches('*') &&
queryParameters['resize'].isNullOrMatches('*')) {
queryParameters['h'] = height.floor().toString();
queryParameters['w'] = width.floor().toString();
queryParameters['crop'] = crop;
queryParameters['resize'] = resize;
}
} else if (fromStreamDashboard) {
queryParameters['height'] = height.floor().toString();
queryParameters['width'] = width.floor().toString();
queryParameters['fit'] = crop;
}
return uri.replace(queryParameters: queryParameters).toString();
}
}
/// List extension
@@ -281,3 +321,10 @@ extension UriX on Uri {
return Uri.parse('http://${toString()}');
}
}
/// Extensions on generic type [T]
extension TypeX<T> on T? {
/// Returns true if the value is null or matches the given [value]
/// otherwise returns false.
bool isNullOrMatches(T value) => this == null || this == value;
}
@@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -64,36 +65,21 @@ class StreamFullScreenMedia extends StatefulWidget {
_StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
}
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia> {
late final PageController _pageController;
late final _curvedAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
late final _currentPage = ValueNotifier(widget.startIndex);
late final _isDisplayingDetail = ValueNotifier<bool>(true);
final _opacityTween = Tween<double>(begin: 1, end: 0);
late final _opacityAnimation = _opacityTween.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.6, curve: Curves.easeOut),
),
);
late final ValueNotifier<int> _currentPage = ValueNotifier(widget.startIndex);
void switchDisplayingDetail() {
_isDisplayingDetail.value = !_isDisplayingDetail.value;
}
final videoPackages = <String, VideoPackage>{};
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_pageController = PageController(initialPage: widget.startIndex);
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
final attachment = widget.mediaAttachmentPackages[i].attachment;
@@ -127,26 +113,102 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
@override
Widget build(BuildContext context) => Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
PageView.builder(
body: ValueListenableBuilder<int>(
valueListenable: _currentPage,
builder: (context, currentPage, child) {
final _currentAttachmentPackage =
widget.mediaAttachmentPackages[currentPage];
final _currentMessage = _currentAttachmentPackage.message;
final _currentAttachment = _currentAttachmentPackage.attachment;
return Stack(
children: [
Positioned.fill(child: child!),
ValueListenableBuilder<bool>(
valueListenable: _isDisplayingDetail,
builder: (context, isDisplayingDetail, child) {
final mediaQuery = MediaQuery.of(context);
final topPadding = mediaQuery.padding.top;
return AnimatedPositionedDirectional(
duration: kThemeAnimationDuration,
curve: Curves.easeInOut,
top: isDisplayingDetail
? 0
: -(topPadding + kToolbarHeight),
start: 0,
end: 0,
height: topPadding + kToolbarHeight,
child: StreamGalleryHeader(
userName: widget.userName,
sentAt: context.translations.sentAtText(
date: _currentAttachmentPackage.message.createdAt,
time: _currentAttachmentPackage.message.createdAt,
),
onBackPressed: Navigator.of(context).pop,
message: _currentMessage,
attachment: _currentAttachment,
onShowMessage: () {
widget.onShowMessage?.call(
_currentMessage,
StreamChannel.of(context).channel,
);
},
attachmentActionsModalBuilder:
widget.attachmentActionsModalBuilder,
),
);
},
),
if (!_currentMessage.isEphemeral)
ValueListenableBuilder<bool>(
valueListenable: _isDisplayingDetail,
builder: (context, isDisplayingDetail, child) {
final mediaQuery = MediaQuery.of(context);
final bottomPadding = mediaQuery.padding.bottom;
return AnimatedPositionedDirectional(
duration: kThemeAnimationDuration,
curve: Curves.easeInOut,
bottom: isDisplayingDetail
? 0
: -(bottomPadding + kToolbarHeight),
start: 0,
end: 0,
height: bottomPadding + kToolbarHeight,
child: StreamGalleryFooter(
currentPage: currentPage,
totalPages: widget.mediaAttachmentPackages.length,
mediaAttachmentPackages:
widget.mediaAttachmentPackages,
mediaSelectedCallBack: (val) {
_currentPage.value = val;
_pageController.animateToPage(
val,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
Navigator.pop(context);
},
),
);
},
),
],
);
},
child: InkWell(
onTap: switchDisplayingDetail,
child: PageView.builder(
controller: _pageController,
itemCount: widget.mediaAttachmentPackages.length,
onPageChanged: (val) {
_currentPage.value = val;
if (videoPackages.isEmpty) {
return;
}
if (videoPackages.isEmpty) return;
final currentAttachment =
widget.mediaAttachmentPackages[val].attachment;
for (final e in videoPackages.values) {
if (e._attachment != currentAttachment) {
e._chewieController?.pause();
}
}
if (widget.autoplayVideos &&
currentAttachment.type == 'video') {
final controller = videoPackages[currentAttachment.id]!;
@@ -161,33 +223,44 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
final imageUrl = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
return AnimatedBuilder(
animation: _curvedAnimation,
builder: (context, child) => PhotoView(
loadingBuilder: (context, image) => const Offstage(),
imageProvider: (imageUrl == null &&
attachment.localUri != null &&
attachment.file?.bytes != null)
? Image.memory(attachment.file!.bytes!).image
: CachedNetworkImageProvider(imageUrl!),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachmentPackages,
return ValueListenableBuilder<bool>(
valueListenable: _isDisplayingDetail,
builder: (context, isDisplayingDetail, _) =>
AnimatedContainer(
color: isDisplayingDetail
? StreamChannelHeaderTheme.of(context).color
: Colors.black,
duration: kThemeAnimationDuration,
child: PhotoView(
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,
);
},
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachmentPackages,
),
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: StreamChannelHeaderTheme.of(context).color,
end: Colors.black,
).lerp(_curvedAnimation.value),
),
onTapUp: (a, b, c) {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
},
),
);
} else if (attachment.type == 'video') {
@@ -198,17 +271,9 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
);
}
return InkWell(
onTap: () {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
},
onTap: switchDisplayingDetail,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50,
),
padding: const EdgeInsets.symmetric(vertical: 50),
child: Chewie(
controller: controller.chewieController!,
),
@@ -217,76 +282,16 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
}
return const SizedBox();
},
itemCount: widget.mediaAttachmentPackages.length,
),
FadeTransition(
opacity: _opacityAnimation,
child: ValueListenableBuilder<int>(
valueListenable: _currentPage,
builder: (context, value, child) {
final _currentAttachmentPackage =
widget.mediaAttachmentPackages[value];
final _currentMessage = _currentAttachmentPackage.message;
final _currentAttachment =
_currentAttachmentPackage.attachment;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamGalleryHeader(
userName: widget.userName,
sentAt: context.translations.sentAtText(
date: widget
.mediaAttachmentPackages[_currentPage.value]
.message
.createdAt,
time: widget
.mediaAttachmentPackages[_currentPage.value]
.message
.createdAt,
),
onBackPressed: () {
Navigator.of(context).pop();
},
message: _currentMessage,
attachment: _currentAttachment,
onShowMessage: () {
widget.onShowMessage?.call(
_currentMessage,
StreamChannel.of(context).channel,
);
},
attachmentActionsModalBuilder:
widget.attachmentActionsModalBuilder,
),
if (!_currentMessage.isEphemeral)
StreamGalleryFooter(
currentPage: value,
totalPages: widget.mediaAttachmentPackages.length,
mediaAttachmentPackages:
widget.mediaAttachmentPackages,
mediaSelectedCallBack: (val) {
_currentPage.value = val;
_pageController.animateToPage(
val,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
Navigator.pop(context);
},
),
],
);
},
),
),
],
),
),
);
@override
void dispose() {
_animationController.dispose();
_currentPage.dispose();
_pageController.dispose();
_isDisplayingDetail.dispose();
for (final package in videoPackages.values) {
package.dispose();
}
@@ -19,6 +19,9 @@ class StreamImageGroup extends StatelessWidget {
this.onReturnAction,
this.onShowMessage,
this.onAttachmentTap,
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'crop',
this.imageThumbnailCropType = 'center',
});
/// List of attachments to show
@@ -36,12 +39,25 @@ class StreamImageGroup extends StatelessWidget {
/// [StreamMessageThemeData] to apply to message
final StreamMessageThemeData messageTheme;
/// Size of iamges
/// Size of images
final Size size;
/// Callback for when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Size of the attachment image thumbnail.
final Size imageThumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
final String /*clip|crop|scale|fill*/ imageThumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
@override
Widget build(BuildContext context) => ConstrainedBox(
constraints: BoxConstraints.loose(size),
@@ -153,5 +169,8 @@ class StreamImageGroup extends StatelessWidget {
message: message,
messageTheme: messageTheme,
onAttachmentTap: () => _onTap(context, index),
imageThumbnailSize: imageThumbnailSize,
imageThumbnailResizeType: imageThumbnailResizeType,
imageThumbnailCropType: imageThumbnailCropType,
);
}
@@ -22,6 +22,10 @@ class StreamMediaListView extends StatefulWidget {
this.selectedIds = const [],
this.onSelect,
this.controller,
this.thumbnailSize = const ThumbnailSize(400, 400),
this.thumbnailFormat = ThumbnailFormat.jpeg,
this.thumbnailQuality = 100,
this.thumbnailScale = 1,
});
/// Stores the media selected
@@ -33,6 +37,21 @@ class StreamMediaListView extends StatefulWidget {
/// Controller that handles MediaListView
final MediaListViewController? controller;
/// The thumbnail size.
final ThumbnailSize thumbnailSize;
/// {@macro photo_manager.ThumbnailFormat}
final ThumbnailFormat thumbnailFormat;
/// The quality value for the thumbnail.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int thumbnailQuality;
/// Scale of the image.
final double thumbnailScale;
@override
_StreamMediaListViewState createState() => _StreamMediaListViewState();
}
@@ -75,14 +94,18 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
AspectRatio(
aspectRatio: 1,
child: FadeInImage(
image: MediaThumbnailProvider(
media: media,
size: widget.thumbnailSize,
format: widget.thumbnailFormat,
quality: widget.thumbnailQuality,
scale: widget.thumbnailScale,
),
fadeInDuration: const Duration(milliseconds: 300),
placeholder: const AssetImage(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
image: MediaThumbnailProvider(
media: media,
),
fit: BoxFit.cover,
),
),
@@ -195,55 +218,95 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
}
}
/// ImageProvider implementation
/// ImageProvider implementation for [AssetEntity].
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
/// Constructor for creating a [MediaThumbnailProvider]
const MediaThumbnailProvider({
required this.media,
// TODO: Are these sizes optimal? Consider web/desktop
this.size = const ThumbnailSize(400, 400),
this.format = ThumbnailFormat.jpeg,
this.quality = 100,
this.scale = 2,
});
/// Media to load
/// Media to get the thumbnail from.
final AssetEntity media;
/// The thumbnail size.
final ThumbnailSize size;
/// {@macro photo_manager.ThumbnailFormat}
final ThumbnailFormat format;
/// The quality value for the thumbnail.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int quality;
/// Scale of the image.
final double scale;
@override
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MediaThumbnailProvider>(this);
}
@override
ImageStreamCompleter load(
MediaThumbnailProvider key,
DecoderCallback decode,
) =>
MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1,
informationCollector: () sync* {
yield ErrorDescription('Id: ${media.id}');
},
);
) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>(
'Thumbnail provider: $this \n Thumbnail key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
}
Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key,
DecoderCallback decode,
) async {
assert(key == this, 'Checks MediaThumbnailProvider');
final bytes = await media.thumbnailData;
assert(key == this, '$key is not $this');
final bytes = await media.thumbnailDataWithSize(
size,
format: format,
quality: quality,
);
return decode(bytes!);
}
@override
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<MediaThumbnailProvider>(this);
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final MediaThumbnailProvider typedOther = other;
return media.id == typedOther.media.id;
if (other is MediaThumbnailProvider) {
return media == other.media &&
size == other.size &&
format == other.format &&
quality == other.quality &&
scale == other.scale;
}
return false;
}
@override
int get hashCode => media.id.hashCode;
int get hashCode => hashValues(media, size, format, quality, scale);
@override
String toString() => '$runtimeType("${media.id}")';
String toString() => '$runtimeType('
'media: $media, '
'size: $size, '
'format: $format, '
'quality: $quality, '
'scale: $scale'
')';
}
extension on Duration {
@@ -252,7 +315,6 @@ extension on Duration {
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
@@ -105,6 +105,9 @@ class StreamMessageWidget extends StatefulWidget {
this.customActions = const [],
this.onAttachmentTap,
this.usernameBuilder,
this.imageAttachmentThumbnailSize = const Size(400, 400),
this.imageAttachmentThumbnailResizeType = 'crop',
this.imageAttachmentThumbnailCropType = 'center',
}) : attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
@@ -130,6 +133,10 @@ class StreamMessageWidget extends StatefulWidget {
onShowMessage: onShowMessage,
onReturnAction: onReturnAction,
onAttachmentTap: onAttachmentTap,
imageThumbnailSize: imageAttachmentThumbnailSize,
imageThumbnailResizeType:
imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
),
),
border,
@@ -155,6 +162,9 @@ class StreamMessageWidget extends StatefulWidget {
onAttachmentTap.call(message, attachments[0]);
}
: null,
imageThumbnailSize: imageAttachmentThumbnailSize,
imageThumbnailResizeType: imageAttachmentThumbnailResizeType,
imageThumbnailCropType: imageAttachmentThumbnailCropType,
),
border,
reverse,
@@ -413,6 +423,19 @@ class StreamMessageWidget extends StatefulWidget {
/// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Size of the image attachment thumbnail.
final Size imageAttachmentThumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageAttachmentThumbnailCropType;
/// Creates a copy of [StreamMessageWidget] with
/// specified attributes overridden.
StreamMessageWidget copyWith({
@@ -468,6 +491,9 @@ class StreamMessageWidget extends StatefulWidget {
List<StreamMessageAction>? customActions,
void Function(Message message, Attachment attachment)? onAttachmentTap,
Widget Function(BuildContext, User)? userAvatarBuilder,
Size? imageAttachmentThumbnailSize,
String? imageAttachmentThumbnailResizeType,
String? imageAttachmentThumbnailCropType,
}) =>
StreamMessageWidget(
key: key ?? this.key,
@@ -528,6 +554,13 @@ class StreamMessageWidget extends StatefulWidget {
customActions: customActions ?? this.customActions,
onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap,
userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder,
imageAttachmentThumbnailSize:
imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize,
imageAttachmentThumbnailResizeType:
imageAttachmentThumbnailResizeType ??
this.imageAttachmentThumbnailResizeType,
imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ??
this.imageAttachmentThumbnailCropType,
);
@override
@@ -43,6 +43,10 @@ class StreamAttachmentPicker extends StatefulWidget {
DefaultAttachmentTypes.video,
],
this.customAttachmentTypes = const [],
this.attachmentThumbnailSize = const ThumbnailSize(400, 400),
this.attachmentThumbnailFormat = ThumbnailFormat.jpeg,
this.attachmentThumbnailQuality = 100,
this.attachmentThumbnailScale = 1,
});
/// True if the picker is open.
@@ -77,6 +81,25 @@ class StreamAttachmentPicker extends StatefulWidget {
/// The list of custom attachment types that can be picked.
final List<CustomAttachmentType> customAttachmentTypes;
/// Size of the attachment thumbnails.
///
/// Defaults to (400, 400).
final ThumbnailSize attachmentThumbnailSize;
/// Format of the attachment thumbnails.
///
/// Defaults to [ThumbnailFormat.jpeg].
final ThumbnailFormat attachmentThumbnailFormat;
/// The quality value for the attachment thumbnails.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int attachmentThumbnailQuality;
/// The scale to apply on the [attachmentThumbnailSize].
final double attachmentThumbnailScale;
/// Used to create a new copy of [StreamAttachmentPicker] with modified
/// properties.
StreamAttachmentPicker copyWith({
@@ -91,7 +114,11 @@ class StreamAttachmentPicker extends StatefulWidget {
ValueChanged<bool>? onChangeInputState,
ValueChanged<String>? onError,
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
List<CustomAttachmentType>? customAttachmentTypes = const [],
List<CustomAttachmentType>? customAttachmentTypes,
ThumbnailSize? attachmentThumbnailSize,
ThumbnailFormat? attachmentThumbnailFormat,
int? attachmentThumbnailQuality,
double? attachmentThumbnailScale,
}) =>
StreamAttachmentPicker(
key: key ?? this.key,
@@ -109,6 +136,14 @@ class StreamAttachmentPicker extends StatefulWidget {
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
customAttachmentTypes:
customAttachmentTypes ?? this.customAttachmentTypes,
attachmentThumbnailSize:
attachmentThumbnailSize ?? this.attachmentThumbnailSize,
attachmentThumbnailFormat:
attachmentThumbnailFormat ?? this.attachmentThumbnailFormat,
attachmentThumbnailQuality:
attachmentThumbnailQuality ?? this.attachmentThumbnailQuality,
attachmentThumbnailScale:
attachmentThumbnailScale ?? this.attachmentThumbnailScale,
);
@override
@@ -360,6 +395,11 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
},
allowedAttachmentTypes: widget.allowedAttachmentTypes,
customAttachmentTypes: widget.customAttachmentTypes,
mediaThumbnailSize: widget.attachmentThumbnailSize,
mediaThumbnailFormat: widget.attachmentThumbnailFormat,
mediaThumbnailQuality:
widget.attachmentThumbnailQuality,
mediaThumbnailScale: widget.attachmentThumbnailScale,
),
),
),
@@ -374,9 +414,11 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
void _addAssetAttachment(AssetEntity medium) async {
final mediaFile = await medium.originFile;
if (mediaFile == null) return;
final tempDir = await getTemporaryDirectory();
// TODO: Confirm that this max resolution is final
// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing
const maxCDNImageResolution = 16800000;
final imageResolution = medium.width * medium.height;
File? cachedFile;
@@ -388,12 +430,13 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
updatedSize.width.floor(),
updatedSize.height.floor(),
),
quality: 30, // TODO: investigate compressing all images
quality: 70, // TODO: investigate compressing all images
);
cachedFile =
final file =
await File('${tempDir.path}/${mediaFile.path.split('/').last}')
.create()
..writeAsBytesSync(resizedImage!);
.create();
file.writeAsBytesSync(resizedImage!);
cachedFile = file;
} else {
cachedFile = await mediaFile
.copy('${tempDir.path}/${mediaFile.path.split('/').last}');
@@ -457,6 +500,10 @@ class _PickerWidget extends StatefulWidget {
required this.allowedAttachmentTypes,
required this.customAttachmentTypes,
required this.mediaListViewController,
this.mediaThumbnailSize = const ThumbnailSize(400, 400),
this.mediaThumbnailFormat = ThumbnailFormat.jpeg,
this.mediaThumbnailQuality = 100,
this.mediaThumbnailScale = 1,
});
final int filePickerIndex;
@@ -468,6 +515,10 @@ class _PickerWidget extends StatefulWidget {
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
final List<CustomAttachmentType> customAttachmentTypes;
final MediaListViewController mediaListViewController;
final ThumbnailSize mediaThumbnailSize;
final ThumbnailFormat mediaThumbnailFormat;
final int mediaThumbnailQuality;
final double mediaThumbnailScale;
@override
_PickerWidgetState createState() => _PickerWidgetState();
@@ -522,6 +573,10 @@ class _PickerWidgetState extends State<_PickerWidget> {
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
controller: widget.mediaListViewController,
thumbnailSize: widget.mediaThumbnailSize,
thumbnailFormat: widget.mediaThumbnailFormat,
thumbnailQuality: widget.mediaThumbnailQuality,
thumbnailScale: widget.mediaThumbnailScale,
);
}
@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/commands_overlay.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
@@ -1,4 +1,6 @@
export 'package:jiffy/jiffy.dart';
export 'package:photo_manager/photo_manager.dart'
show ThumbnailSize, ThumbnailFormat;
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
@@ -22,6 +24,7 @@ export 'src/info_tile.dart';
export 'src/localization/stream_chat_localizations.dart';
export 'src/localization/translations.dart' show DefaultTranslations;
export 'src/message_action.dart';
// ignore: deprecated_member_use_from_same_package
export 'src/message_input.dart' show MessageInput, MessageInputState;
export 'src/message_list_view.dart';
@@ -49,6 +52,7 @@ export 'src/user_item.dart';
export 'src/user_list_view.dart';
export 'src/user_mention_tile.dart';
export 'src/utils.dart';
// v4
export 'src/v4/message_input/countdown_button.dart';
export 'src/v4/message_input/stream_attachment_picker.dart';