From 1e9e942c467be149369f40f5121a1848b2dd97d4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 24 Oct 2022 14:57:46 +0530 Subject: [PATCH] feat(llc, ui): add support for thumbUrl in video attachment. (#1362) * feat(llc, ui): add support for thumbUrl in file attachment. Signed-off-by: xsahil03x * chore(ui): use widget.fit instead of hardcoded fit. Signed-off-by: xsahil03x * fix analysis Signed-off-by: xsahil03x Co-authored-by: Salvatore Giordano --- packages/stream_chat/CHANGELOG.md | 4 + .../stream_chat/lib/src/client/channel.dart | 19 +++-- .../lib/src/core/api/responses.dart | 29 ++++--- .../lib/src/core/api/responses.g.dart | 14 ++-- packages/stream_chat_flutter/CHANGELOG.md | 10 ++- .../lib/src/attachment/file_attachment.dart | 2 - .../lib/src/attachment/video_attachment.dart | 6 +- .../lib/src/gallery/gallery_footer.dart | 1 - .../message_input/quoted_message_widget.dart | 1 - .../message_input/stream_message_input.dart | 1 - .../lib/src/video/video_thumbnail_image.dart | 79 ++++++++++++------- 11 files changed, 101 insertions(+), 65 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 1017ec7d..fe3b08f1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,5 +1,9 @@ ## Upcoming +✅ Added + +- Added `thumbUrl` field in `SendFileResponse` model. + 🐞 Fixed - Remove disposed channel clients from the client state. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index cc2550f7..bfaae76a 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -493,36 +493,41 @@ class Channel { final isImage = it.type == 'image'; final cancelToken = CancelToken(); - Future future; + Future future; if (isImage) { future = sendImage( it.file!, onSendProgress: onSendProgress, cancelToken: cancelToken, extraData: it.extraData, - ).then((it) => it.file); + ); } else { future = sendFile( it.file!, onSendProgress: onSendProgress, cancelToken: cancelToken, extraData: it.extraData, - ).then((it) => it.file); + ); } _cancelableAttachmentUploadRequest[it.id] = cancelToken; - return future.then((url) { + return future.then((response) { client.logger.info('Attachment ${it.id} uploaded successfully...'); - if (isImage) { + + // If the response is SendFileResponse, then we might also be getting + // thumbUrl in case of video. So we need to update the attachment with + // both the assetUrl and thumbUrl. + if (response is SendFileResponse) { updateAttachment( it.copyWith( - imageUrl: url, + assetUrl: response.file, + thumbUrl: response.thumbUrl, uploadState: const UploadState.success(), ), ); } else { updateAttachment( it.copyWith( - assetUrl: url, + imageUrl: response.file, uploadState: const UploadState.success(), ), ); diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 54b885f4..53576dfe 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -157,11 +157,24 @@ class ListDevicesResponse extends _BaseResponse { _$ListDevicesResponseFromJson(json); } +/// Base Model response for [Channel.sendImage] and [Channel.sendFile] api call. +@JsonSerializable(createToJson: false) +class SendAttachmentResponse extends _BaseResponse { + /// The url of the uploaded attachment. + late String? file; + + /// Create a new instance from a json + static SendAttachmentResponse fromJson(Map json) => + _$SendAttachmentResponseFromJson(json); +} + /// Model response for [Channel.sendFile] api call @JsonSerializable(createToJson: false) -class SendFileResponse extends _BaseResponse { - /// The url of the uploaded file - late String file; +class SendFileResponse extends SendAttachmentResponse { + /// The url of the uploaded video file. + /// + /// This is only present if the file is a video. + String? thumbUrl; /// Create a new instance from a json static SendFileResponse fromJson(Map json) => @@ -169,15 +182,7 @@ class SendFileResponse extends _BaseResponse { } /// Model response for [Channel.sendImage] api call -@JsonSerializable(createToJson: false) -class SendImageResponse extends _BaseResponse { - /// The url of the uploaded file - late String file; - - /// Create a new instance from a json - static SendImageResponse fromJson(Map json) => - _$SendImageResponseFromJson(json); -} +typedef SendImageResponse = SendAttachmentResponse; /// Model response for [Channel.sendReaction] api call @JsonSerializable(createToJson: false) diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 24f5c459..624f533b 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -97,15 +97,17 @@ ListDevicesResponse _$ListDevicesResponseFromJson(Map json) => .toList() ?? []; +SendAttachmentResponse _$SendAttachmentResponseFromJson( + Map json) => + SendAttachmentResponse() + ..duration = json['duration'] as String? + ..file = json['file'] as String?; + SendFileResponse _$SendFileResponseFromJson(Map json) => SendFileResponse() ..duration = json['duration'] as String? - ..file = json['file'] as String; - -SendImageResponse _$SendImageResponseFromJson(Map json) => - SendImageResponse() - ..duration = json['duration'] as String? - ..file = json['file'] as String; + ..file = json['file'] as String? + ..thumbUrl = json['thumb_url'] as String?; SendReactionResponse _$SendReactionResponseFromJson( Map json) => diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5d34904f..92266e73 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,5 +1,9 @@ ## Upcoming +✅ Added + +- `VideoAttachment` now uses `thumbUrl` to show the thumbnail + if it's available instead of generating them. - Expose `widthFactor` option in `MessageWidget` ## 5.0.1 @@ -555,7 +559,7 @@ typedef ActionButtonBuilder = Widget Function( ``` > **_NOTE:_** The last parameter is the default `ActionButton` -You can call `.copyWith` to customize just a subset of properties. +> You can call `.copyWith` to customize just a subset of properties. - Added slow mode which allows a cooldown period after a user sends a message. @@ -658,7 +662,7 @@ typedef MessageBuilder = Widget Function( ``` > **_NOTE:_** the last parameter is the default `MessageWidget` -You can call `.copyWith` to customize just a subset of properties +> You can call `.copyWith` to customize just a subset of properties ✅ Added @@ -728,7 +732,7 @@ typedef MessageBuilder = Widget Function( ``` > **_NOTE:_** The last parameter is the default `MessageWidget` -You can call `.copyWith` to customize just a subset of properties. +> You can call `.copyWith` to customize just a subset of properties. ✅ Added diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index ff11fd46..2e33ff0f 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -195,7 +195,6 @@ class _FileTypeImage extends StatelessWidget { shape: _getDefaultShape(context), child: source.when( local: () => StreamVideoThumbnailImage( - fit: BoxFit.cover, video: attachment.file!.path!, placeholderBuilder: (_) => const Center( child: SizedBox( @@ -206,7 +205,6 @@ class _FileTypeImage extends StatelessWidget { ), ), network: () => StreamVideoThumbnailImage( - fit: BoxFit.cover, video: attachment.assetUrl!, placeholderBuilder: (_) => const Center( child: SizedBox( diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index 4ac9005c..fd0e3a40 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -42,9 +42,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget { context, StreamVideoThumbnailImage( video: attachment.file!.path!, + thumbUrl: attachment.thumbUrl, constraints: constraints, - fit: BoxFit.cover, - errorBuilder: (_, __) => AttachmentError(constraints: constraints), ), ); }, @@ -56,9 +55,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget { context, StreamVideoThumbnailImage( video: attachment.assetUrl!, + thumbUrl: attachment.thumbUrl, constraints: constraints, - fit: BoxFit.cover, - errorBuilder: (_, __) => AttachmentError(constraints: constraints), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart index 431ab2ad..3072c0ca 100644 --- a/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart @@ -221,7 +221,6 @@ class _StreamGalleryFooterState extends State { child: StreamVideoThumbnailImage( video: (attachment.file?.path ?? attachment.assetUrl)!, - fit: BoxFit.cover, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index bee19bf6..c0b06c87 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -278,7 +278,6 @@ class _ParseAttachments extends StatelessWidget { key: ValueKey(attachment.assetUrl), video: attachment.file?.path ?? attachment.assetUrl!, constraints: BoxConstraints.loose(const Size(32, 32)), - fit: BoxFit.cover, errorBuilder: (_, __) => AttachmentError( constraints: BoxConstraints.loose(const Size(32, 32)), ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index 75902313..130d0462 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -1191,7 +1191,6 @@ class StreamMessageInputState extends State ), ), video: (attachment.file?.path ?? attachment.assetUrl)!, - fit: BoxFit.cover, ), Positioned( left: 8, diff --git a/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart index f6a6ed56..83871581 100644 --- a/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/video/video_service.dart'; @@ -14,16 +15,20 @@ class StreamVideoThumbnailImage extends StatefulWidget { const StreamVideoThumbnailImage({ super.key, required this.video, + this.thumbUrl, this.constraints, - this.fit, + this.fit = BoxFit.cover, this.format = ImageFormat.PNG, this.errorBuilder, this.placeholderBuilder, }); - /// Video path + /// Video path or url final String video; + /// Video thumbnail url + final String? thumbUrl; + /// Contraints of attachments final BoxConstraints? constraints; @@ -49,13 +54,20 @@ class _StreamVideoThumbnailImageState extends State { late Future thumbnailFuture; late StreamChatThemeData _streamChatTheme; + void _generateThumbnail() { + // Only generate thumbnail if the thumbnail url is not provided. + if (widget.thumbUrl == null) { + thumbnailFuture = StreamVideoService.generateVideoThumbnail( + video: widget.video, + imageFormat: widget.format, + ); + } + } + @override void initState() { super.initState(); - thumbnailFuture = StreamVideoService.generateVideoThumbnail( - video: widget.video, - imageFormat: widget.format, - ); + _generateThumbnail(); } @override @@ -67,16 +79,41 @@ class _StreamVideoThumbnailImageState extends State { @override void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) { if (oldWidget.video != widget.video || oldWidget.format != widget.format) { - thumbnailFuture = StreamVideoService.generateVideoThumbnail( - video: widget.video, - imageFormat: widget.format, - ); + _generateThumbnail(); } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { + final placeHolderWidget = widget.placeholderBuilder?.call(context) ?? + Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + height: widget.constraints?.maxHeight, + width: widget.constraints?.maxWidth, + package: 'stream_chat_flutter', + ), + ); + + final errorWidget = widget.errorBuilder?.call(context, null) ?? + AttachmentError(constraints: widget.constraints); + + final thumbUrl = widget.thumbUrl; + if (thumbUrl != null) { + return CachedNetworkImage( + imageUrl: thumbUrl, + fit: widget.fit, + height: widget.constraints?.maxHeight, + width: widget.constraints?.maxWidth, + placeholder: (context, __) => placeHolderWidget, + errorWidget: (context, url, error) => errorWidget, + ); + } + return ConstrainedBox( constraints: widget.constraints ?? const BoxConstraints.expand(), child: FutureBuilder( @@ -86,30 +123,16 @@ class _StreamVideoThumbnailImageState extends State { child: Builder( key: ValueKey>(snapshot), builder: (_) { - if (snapshot.hasError) { - return widget.errorBuilder?.call(context, snapshot.error) ?? - Center( - child: StreamSvgIcon.error(), - ); - } + if (snapshot.hasError) return errorWidget; + if (!snapshot.hasData) { return SizedBox( height: double.maxFinite, width: double.maxFinite, - child: widget.placeholderBuilder?.call(context) ?? - Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.disabled, - highlightColor: _streamChatTheme.colorTheme.inputBg, - child: Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - height: widget.constraints?.maxHeight, - width: widget.constraints?.maxWidth, - package: 'stream_chat_flutter', - ), - ), + child: placeHolderWidget, ); } + return SizedBox( height: double.maxFinite, width: double.maxFinite,