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 <xdsahil@gmail.com> * chore(ui): use widget.fit instead of hardcoded fit. Signed-off-by: xsahil03x <xdsahil@gmail.com> * fix analysis Signed-off-by: xsahil03x <xdsahil@gmail.com> Co-authored-by: Salvatore Giordano <salvatoregiordanoo@gmail.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
## Upcoming
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `thumbUrl` field in `SendFileResponse` model.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Remove disposed channel clients from the client state.
|
||||
|
||||
@@ -493,36 +493,41 @@ class Channel {
|
||||
|
||||
final isImage = it.type == 'image';
|
||||
final cancelToken = CancelToken();
|
||||
Future<String> future;
|
||||
Future<SendAttachmentResponse> 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(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$SendImageResponseFromJson(json);
|
||||
}
|
||||
typedef SendImageResponse = SendAttachmentResponse;
|
||||
|
||||
/// Model response for [Channel.sendReaction] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
|
||||
@@ -97,15 +97,17 @@ ListDevicesResponse _$ListDevicesResponseFromJson(Map<String, dynamic> json) =>
|
||||
.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()
|
||||
..duration = json['duration'] as String?
|
||||
..file = json['file'] as String;
|
||||
|
||||
SendImageResponse _$SendImageResponseFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -221,7 +221,6 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
|
||||
child: StreamVideoThumbnailImage(
|
||||
video: (attachment.file?.path ??
|
||||
attachment.assetUrl)!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -1191,7 +1191,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
||||
),
|
||||
),
|
||||
video: (attachment.file?.path ?? attachment.assetUrl)!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
|
||||
@@ -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<StreamVideoThumbnailImage> {
|
||||
late Future<Uint8List?> 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<StreamVideoThumbnailImage> {
|
||||
@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<Uint8List?>(
|
||||
@@ -86,30 +123,16 @@ class _StreamVideoThumbnailImageState extends State<StreamVideoThumbnailImage> {
|
||||
child: Builder(
|
||||
key: ValueKey<AsyncSnapshot<Uint8List?>>(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,
|
||||
|
||||
Reference in New Issue
Block a user