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 <[email protected]>

* chore(ui): use widget.fit instead of hardcoded fit.

Signed-off-by: xsahil03x <[email protected]>

* fix analysis

Signed-off-by: xsahil03x <[email protected]>
Co-authored-by: Salvatore Giordano <[email protected]>
This commit is contained in:
Sahil Kumar
2022-10-24 11:27:46 +02:00
committed by GitHub
co-authored by Salvatore Giordano
parent 8b6c59929f
commit 1e9e942c46
11 changed files with 101 additions and 65 deletions
+4
View File
@@ -1,5 +1,9 @@
## Upcoming ## Upcoming
✅ Added
- Added `thumbUrl` field in `SendFileResponse` model.
🐞 Fixed 🐞 Fixed
- Remove disposed channel clients from the client state. - Remove disposed channel clients from the client state.
@@ -493,36 +493,41 @@ class Channel {
final isImage = it.type == 'image'; final isImage = it.type == 'image';
final cancelToken = CancelToken(); final cancelToken = CancelToken();
Future<String> future; Future<SendAttachmentResponse> future;
if (isImage) { if (isImage) {
future = sendImage( future = sendImage(
it.file!, it.file!,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
cancelToken: cancelToken, cancelToken: cancelToken,
extraData: it.extraData, extraData: it.extraData,
).then((it) => it.file); );
} else { } else {
future = sendFile( future = sendFile(
it.file!, it.file!,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
cancelToken: cancelToken, cancelToken: cancelToken,
extraData: it.extraData, extraData: it.extraData,
).then((it) => it.file); );
} }
_cancelableAttachmentUploadRequest[it.id] = cancelToken; _cancelableAttachmentUploadRequest[it.id] = cancelToken;
return future.then((url) { return future.then((response) {
client.logger.info('Attachment ${it.id} uploaded successfully...'); 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( updateAttachment(
it.copyWith( it.copyWith(
imageUrl: url, assetUrl: response.file,
thumbUrl: response.thumbUrl,
uploadState: const UploadState.success(), uploadState: const UploadState.success(),
), ),
); );
} else { } else {
updateAttachment( updateAttachment(
it.copyWith( it.copyWith(
assetUrl: url, imageUrl: response.file,
uploadState: const UploadState.success(), uploadState: const UploadState.success(),
), ),
); );
@@ -157,11 +157,24 @@ class ListDevicesResponse extends _BaseResponse {
_$ListDevicesResponseFromJson(json); _$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<String, dynamic> json) =>
_$SendAttachmentResponseFromJson(json);
}
/// Model response for [Channel.sendFile] api call /// Model response for [Channel.sendFile] api call
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class SendFileResponse extends _BaseResponse { class SendFileResponse extends SendAttachmentResponse {
/// The url of the uploaded file /// The url of the uploaded video file.
late String file; ///
/// This is only present if the file is a video.
String? thumbUrl;
/// Create a new instance from a json /// Create a new instance from a json
static SendFileResponse fromJson(Map<String, dynamic> json) => static SendFileResponse fromJson(Map<String, dynamic> json) =>
@@ -169,15 +182,7 @@ class SendFileResponse extends _BaseResponse {
} }
/// Model response for [Channel.sendImage] api call /// Model response for [Channel.sendImage] api call
@JsonSerializable(createToJson: false) typedef SendImageResponse = SendAttachmentResponse;
class SendImageResponse extends _BaseResponse {
/// The url of the uploaded file
late String file;
/// Create a new instance from a json
static SendImageResponse fromJson(Map<String, dynamic> json) =>
_$SendImageResponseFromJson(json);
}
/// Model response for [Channel.sendReaction] api call /// Model response for [Channel.sendReaction] api call
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
@@ -97,15 +97,17 @@ ListDevicesResponse _$ListDevicesResponseFromJson(Map<String, dynamic> json) =>
.toList() ?? .toList() ??
[]; [];
SendAttachmentResponse _$SendAttachmentResponseFromJson(
Map<String, dynamic> json) =>
SendAttachmentResponse()
..duration = json['duration'] as String?
..file = json['file'] as String?;
SendFileResponse _$SendFileResponseFromJson(Map<String, dynamic> json) => SendFileResponse _$SendFileResponseFromJson(Map<String, dynamic> json) =>
SendFileResponse() SendFileResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..file = json['file'] as String; ..file = json['file'] as String?
..thumbUrl = json['thumb_url'] as String?;
SendImageResponse _$SendImageResponseFromJson(Map<String, dynamic> json) =>
SendImageResponse()
..duration = json['duration'] as String?
..file = json['file'] as String;
SendReactionResponse _$SendReactionResponseFromJson( SendReactionResponse _$SendReactionResponseFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
+7 -3
View File
@@ -1,5 +1,9 @@
## Upcoming ## Upcoming
✅ Added
- `VideoAttachment` now uses `thumbUrl` to show the thumbnail
if it's available instead of generating them.
- Expose `widthFactor` option in `MessageWidget` - Expose `widthFactor` option in `MessageWidget`
## 5.0.1 ## 5.0.1
@@ -555,7 +559,7 @@ typedef ActionButtonBuilder = Widget Function(
``` ```
> **_NOTE:_** The last parameter is the default `ActionButton` > **_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. - 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` > **_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 ✅ Added
@@ -728,7 +732,7 @@ typedef MessageBuilder = Widget Function(
``` ```
> **_NOTE:_** The last parameter is the default `MessageWidget` > **_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 ✅ Added
@@ -195,7 +195,6 @@ class _FileTypeImage extends StatelessWidget {
shape: _getDefaultShape(context), shape: _getDefaultShape(context),
child: source.when( child: source.when(
local: () => StreamVideoThumbnailImage( local: () => StreamVideoThumbnailImage(
fit: BoxFit.cover,
video: attachment.file!.path!, video: attachment.file!.path!,
placeholderBuilder: (_) => const Center( placeholderBuilder: (_) => const Center(
child: SizedBox( child: SizedBox(
@@ -206,7 +205,6 @@ class _FileTypeImage extends StatelessWidget {
), ),
), ),
network: () => StreamVideoThumbnailImage( network: () => StreamVideoThumbnailImage(
fit: BoxFit.cover,
video: attachment.assetUrl!, video: attachment.assetUrl!,
placeholderBuilder: (_) => const Center( placeholderBuilder: (_) => const Center(
child: SizedBox( child: SizedBox(
@@ -42,9 +42,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
context, context,
StreamVideoThumbnailImage( StreamVideoThumbnailImage(
video: attachment.file!.path!, video: attachment.file!.path!,
thumbUrl: attachment.thumbUrl,
constraints: constraints, constraints: constraints,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(constraints: constraints),
), ),
); );
}, },
@@ -56,9 +55,8 @@ class StreamVideoAttachment extends StreamAttachmentWidget {
context, context,
StreamVideoThumbnailImage( StreamVideoThumbnailImage(
video: attachment.assetUrl!, video: attachment.assetUrl!,
thumbUrl: attachment.thumbUrl,
constraints: constraints, constraints: constraints,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(constraints: constraints),
), ),
); );
}, },
@@ -221,7 +221,6 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
child: StreamVideoThumbnailImage( child: StreamVideoThumbnailImage(
video: (attachment.file?.path ?? video: (attachment.file?.path ??
attachment.assetUrl)!, attachment.assetUrl)!,
fit: BoxFit.cover,
), ),
), ),
), ),
@@ -278,7 +278,6 @@ class _ParseAttachments extends StatelessWidget {
key: ValueKey(attachment.assetUrl), key: ValueKey(attachment.assetUrl),
video: attachment.file?.path ?? attachment.assetUrl!, video: attachment.file?.path ?? attachment.assetUrl!,
constraints: BoxConstraints.loose(const Size(32, 32)), constraints: BoxConstraints.loose(const Size(32, 32)),
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError( errorBuilder: (_, __) => AttachmentError(
constraints: BoxConstraints.loose(const Size(32, 32)), constraints: BoxConstraints.loose(const Size(32, 32)),
), ),
@@ -1191,7 +1191,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
), ),
), ),
video: (attachment.file?.path ?? attachment.assetUrl)!, video: (attachment.file?.path ?? attachment.assetUrl)!,
fit: BoxFit.cover,
), ),
Positioned( Positioned(
left: 8, left: 8,
@@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/video/video_service.dart'; import 'package:stream_chat_flutter/src/video/video_service.dart';
@@ -14,16 +15,20 @@ class StreamVideoThumbnailImage extends StatefulWidget {
const StreamVideoThumbnailImage({ const StreamVideoThumbnailImage({
super.key, super.key,
required this.video, required this.video,
this.thumbUrl,
this.constraints, this.constraints,
this.fit, this.fit = BoxFit.cover,
this.format = ImageFormat.PNG, this.format = ImageFormat.PNG,
this.errorBuilder, this.errorBuilder,
this.placeholderBuilder, this.placeholderBuilder,
}); });
/// Video path /// Video path or url
final String video; final String video;
/// Video thumbnail url
final String? thumbUrl;
/// Contraints of attachments /// Contraints of attachments
final BoxConstraints? constraints; final BoxConstraints? constraints;
@@ -49,13 +54,20 @@ class _StreamVideoThumbnailImageState extends State<StreamVideoThumbnailImage> {
late Future<Uint8List?> thumbnailFuture; late Future<Uint8List?> thumbnailFuture;
late StreamChatThemeData _streamChatTheme; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
thumbnailFuture = StreamVideoService.generateVideoThumbnail( _generateThumbnail();
video: widget.video,
imageFormat: widget.format,
);
} }
@override @override
@@ -67,16 +79,41 @@ class _StreamVideoThumbnailImageState extends State<StreamVideoThumbnailImage> {
@override @override
void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) { void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) {
if (oldWidget.video != widget.video || oldWidget.format != widget.format) { if (oldWidget.video != widget.video || oldWidget.format != widget.format) {
thumbnailFuture = StreamVideoService.generateVideoThumbnail( _generateThumbnail();
video: widget.video,
imageFormat: widget.format,
);
} }
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@override @override
Widget build(BuildContext context) { 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( return ConstrainedBox(
constraints: widget.constraints ?? const BoxConstraints.expand(), constraints: widget.constraints ?? const BoxConstraints.expand(),
child: FutureBuilder<Uint8List?>( child: FutureBuilder<Uint8List?>(
@@ -86,30 +123,16 @@ class _StreamVideoThumbnailImageState extends State<StreamVideoThumbnailImage> {
child: Builder( child: Builder(
key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot), key: ValueKey<AsyncSnapshot<Uint8List?>>(snapshot),
builder: (_) { builder: (_) {
if (snapshot.hasError) { if (snapshot.hasError) return errorWidget;
return widget.errorBuilder?.call(context, snapshot.error) ??
Center(
child: StreamSvgIcon.error(),
);
}
if (!snapshot.hasData) { if (!snapshot.hasData) {
return SizedBox( return SizedBox(
height: double.maxFinite, height: double.maxFinite,
width: double.maxFinite, width: double.maxFinite,
child: widget.placeholderBuilder?.call(context) ?? child: placeHolderWidget,
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',
),
),
); );
} }
return SizedBox( return SizedBox(
height: double.maxFinite, height: double.maxFinite,
width: double.maxFinite, width: double.maxFinite,