feat: more implementation.
Signed-off-by: xsahil03x <[email protected]>
@@ -499,7 +499,7 @@ class Channel {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isImage = it.type == 'image';
|
final isImage = it.type == AttachmentType.image;
|
||||||
final cancelToken = CancelToken();
|
final cancelToken = CancelToken();
|
||||||
Future<SendAttachmentResponse> future;
|
Future<SendAttachmentResponse> future;
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mixin AttachmentType {
|
|||||||
static const file = 'file';
|
static const file = 'file';
|
||||||
static const giphy = 'giphy';
|
static const giphy = 'giphy';
|
||||||
static const video = 'video';
|
static const video = 'video';
|
||||||
|
static const audio = 'audio';
|
||||||
|
|
||||||
/// Application custom types.
|
/// Application custom types.
|
||||||
static const urlPreview = 'url_preview';
|
static const urlPreview = 'url_preview';
|
||||||
@@ -53,19 +54,15 @@ class Attachment extends Equatable {
|
|||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
_type = type,
|
_type = type,
|
||||||
title = title ?? file?.name,
|
title = title ?? file?.name,
|
||||||
|
_uploadState = uploadState,
|
||||||
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
|
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
|
||||||
// For backwards compatibility,
|
// For backwards compatibility,
|
||||||
// set 'file_size', 'mime_type' in [extraData].
|
// set 'file_size', 'mime_type' in [extraData].
|
||||||
extraData = {
|
extraData = {
|
||||||
...extraData,
|
...extraData,
|
||||||
if (file?.size != null) 'file_size': file?.size,
|
if (file?.size != null) 'file_size': file?.size,
|
||||||
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
|
if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType,
|
||||||
} {
|
};
|
||||||
this.uploadState = uploadState ??
|
|
||||||
((assetUrl != null || imageUrl != null || thumbUrl != null)
|
|
||||||
? const UploadState.success()
|
|
||||||
: const UploadState.preparing());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from a json
|
||||||
factory Attachment.fromJson(Map<String, dynamic> json) =>
|
factory Attachment.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -82,7 +79,8 @@ class Attachment extends Equatable {
|
|||||||
|
|
||||||
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
|
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
|
||||||
Attachment(
|
Attachment(
|
||||||
type: ogAttachment.type,
|
// If the type is not specified, we default to urlPreview.
|
||||||
|
type: ogAttachment.type ?? AttachmentType.urlPreview,
|
||||||
title: ogAttachment.title,
|
title: ogAttachment.title,
|
||||||
titleLink: ogAttachment.titleLink,
|
titleLink: ogAttachment.titleLink,
|
||||||
text: ogAttachment.text,
|
text: ogAttachment.text,
|
||||||
@@ -98,7 +96,9 @@ class Attachment extends Equatable {
|
|||||||
///The attachment type based on the URL resource. This can be: audio,
|
///The attachment type based on the URL resource. This can be: audio,
|
||||||
///image or video
|
///image or video
|
||||||
String? get type {
|
String? get type {
|
||||||
if (_type == AttachmentType.image && titleLink != null) {
|
// If the attachment contains titleLink but is not of type giphy, we
|
||||||
|
// consider it as a urlPreview.
|
||||||
|
if (_type != AttachmentType.giphy && titleLink != null) {
|
||||||
return AttachmentType.urlPreview;
|
return AttachmentType.urlPreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,9 @@ class Attachment extends Equatable {
|
|||||||
|
|
||||||
final String? _type;
|
final String? _type;
|
||||||
|
|
||||||
|
/// The raw attachment type.
|
||||||
|
String? get rawType => _type;
|
||||||
|
|
||||||
///The link to which the attachment message points to.
|
///The link to which the attachment message points to.
|
||||||
final String? titleLink;
|
final String? titleLink;
|
||||||
|
|
||||||
@@ -159,7 +162,15 @@ class Attachment extends Equatable {
|
|||||||
final AttachmentFile? file;
|
final AttachmentFile? file;
|
||||||
|
|
||||||
/// The current upload state of the attachment
|
/// The current upload state of the attachment
|
||||||
late final UploadState uploadState;
|
UploadState get uploadState {
|
||||||
|
if (_uploadState case final state?) return state;
|
||||||
|
|
||||||
|
return ((assetUrl != null || imageUrl != null || thumbUrl != null)
|
||||||
|
? const UploadState.success()
|
||||||
|
: const UploadState.preparing());
|
||||||
|
}
|
||||||
|
|
||||||
|
final UploadState? _uploadState;
|
||||||
|
|
||||||
/// Map of custom channel extraData
|
/// Map of custom channel extraData
|
||||||
final Map<String, Object?> extraData;
|
final Map<String, Object?> extraData;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class AttachmentFile {
|
|||||||
String? get extension => name?.split('.').last;
|
String? get extension => name?.split('.').last;
|
||||||
|
|
||||||
/// The mime type of this file.
|
/// The mime type of this file.
|
||||||
MediaType? get mimeType => name?.mimeType;
|
MediaType? get mediaType => name?.mediaType;
|
||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
|
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
|
||||||
@@ -75,13 +75,13 @@ class AttachmentFile {
|
|||||||
multiPartFile = MultipartFile.fromBytes(
|
multiPartFile = MultipartFile.fromBytes(
|
||||||
bytes!,
|
bytes!,
|
||||||
filename: name,
|
filename: name,
|
||||||
contentType: mimeType,
|
contentType: mediaType,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
multiPartFile = await MultipartFile.fromFile(
|
multiPartFile = await MultipartFile.fromFile(
|
||||||
path!,
|
path!,
|
||||||
filename: name,
|
filename: name,
|
||||||
contentType: mimeType,
|
contentType: mediaType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return multiPartFile;
|
return multiPartFile;
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ extension MapX<K, V> on Map<K?, V?> {
|
|||||||
|
|
||||||
/// Useful extension functions for [String]
|
/// Useful extension functions for [String]
|
||||||
extension StringX on String {
|
extension StringX on String {
|
||||||
/// returns the mime type from the passed file name.
|
/// returns the media type from the passed file name.
|
||||||
MediaType? get mimeType {
|
MediaType? get mediaType {
|
||||||
if (toLowerCase().endsWith('heic')) {
|
if (toLowerCase().endsWith('heic')) {
|
||||||
return MediaType.parse('image/heic');
|
return MediaType.parse('image/heic');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ void main() {
|
|||||||
group('mimeType', () {
|
group('mimeType', () {
|
||||||
test('should return null if `String` is not a filename', () {
|
test('should return null if `String` is not a filename', () {
|
||||||
const fileName = 'not-a-file-name';
|
const fileName = 'not-a-file-name';
|
||||||
final mimeType = fileName.mimeType;
|
final mimeType = fileName.mediaType;
|
||||||
expect(mimeType, isNull);
|
expect(mimeType, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return mimeType if string is a filename', () {
|
test('should return mimeType if string is a filename', () {
|
||||||
const fileName = 'dummyFileName.jpeg';
|
const fileName = 'dummyFileName.jpeg';
|
||||||
final mimeType = fileName.mimeType;
|
final mimeType = fileName.mediaType;
|
||||||
expect(mimeType, isNotNull);
|
expect(mimeType, isNotNull);
|
||||||
expect(mimeType!.type, 'image');
|
expect(mimeType!.type, 'image');
|
||||||
expect(mimeType.subtype, 'jpeg');
|
expect(mimeType.subtype, 'jpeg');
|
||||||
@@ -39,7 +39,7 @@ void main() {
|
|||||||
|
|
||||||
test('should return `image/heic` if ends with `heic`', () {
|
test('should return `image/heic` if ends with `heic`', () {
|
||||||
const fileName = 'dummyFileName.heic';
|
const fileName = 'dummyFileName.heic';
|
||||||
final mimeType = fileName.mimeType;
|
final mimeType = fileName.mediaType;
|
||||||
expect(mimeType, isNotNull);
|
expect(mimeType, isNotNull);
|
||||||
expect(mimeType!.type, 'image');
|
expect(mimeType!.type, 'image');
|
||||||
expect(mimeType.subtype, 'heic');
|
expect(mimeType.subtype, 'heic');
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ class AttachmentWidgetCatalog {
|
|||||||
extension on List<Attachment> {
|
extension on List<Attachment> {
|
||||||
/// Groups the attachments by their type.
|
/// Groups the attachments by their type.
|
||||||
Map<String, List<Attachment>> get grouped {
|
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: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/gallery_attachment.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/giphy_attachment.dart';
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.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/url_attachment.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/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.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/src/utils/utils.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../attachment_upload_state_builder.dart';
|
|
||||||
|
|
||||||
part 'fallback_attachment_builder.dart';
|
part 'fallback_attachment_builder.dart';
|
||||||
|
|
||||||
part 'file_attachment_builder.dart';
|
part 'file_attachment_builder.dart';
|
||||||
@@ -82,41 +75,58 @@ abstract class StreamAttachmentWidgetBuilder {
|
|||||||
/// widget.
|
/// widget.
|
||||||
static List<StreamAttachmentWidgetBuilder> defaultBuilders({
|
static List<StreamAttachmentWidgetBuilder> defaultBuilders({
|
||||||
required Message message,
|
required Message message,
|
||||||
|
ShapeBorder? shape,
|
||||||
|
EdgeInsetsGeometry padding = const EdgeInsets.all(4),
|
||||||
StreamAttachmentWidgetTapCallback? onAttachmentTap,
|
StreamAttachmentWidgetTapCallback? onAttachmentTap,
|
||||||
}) {
|
}) {
|
||||||
return [
|
return [
|
||||||
// Handles a mix of image, gif, video, and file attachments.
|
// Handles a mix of image, gif, video, url and file attachments.
|
||||||
MixedAttachmentBuilder(
|
MixedAttachmentBuilder(
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Handles a mix of image, gif, and video attachments.
|
// Handles a mix of image, gif, and video attachments.
|
||||||
GalleryAttachmentBuilder(
|
GalleryAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
|
runSpacing: padding.vertical / 2,
|
||||||
|
spacing: padding.horizontal / 2,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Handles file attachments.
|
// Handles file attachments.
|
||||||
FileAttachmentBuilder(
|
FileAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Handles giphy attachments.
|
// Handles giphy attachments.
|
||||||
GiphyAttachmentBuilder(
|
GiphyAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Handles image attachments.
|
// Handles image attachments.
|
||||||
ImageAttachmentBuilder(
|
ImageAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Handles video attachments.
|
// Handles video attachments.
|
||||||
VideoAttachmentBuilder(
|
VideoAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
// We don't handle URL attachments if the message is a reply.
|
// We don't handle URL attachments if the message is a reply.
|
||||||
if (message.quotedMessage == null)
|
if (message.quotedMessage == null)
|
||||||
UrlAttachmentBuilder(
|
UrlAttachmentBuilder(
|
||||||
|
shape: shape,
|
||||||
|
padding: padding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -93,16 +93,6 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
|||||||
attachments: galleryAttachments,
|
attachments: galleryAttachments,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final attachment = galleryAttachments[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;
|
VoidCallback? onTap;
|
||||||
if (onAttachmentTap != null) {
|
if (onAttachmentTap != null) {
|
||||||
@@ -112,29 +102,13 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
if (isImage)
|
StreamMediaAttachmentThumbnail(
|
||||||
StreamImageAttachmentThumbnail(
|
media: attachment,
|
||||||
image: attachment,
|
width: constraints.maxWidth,
|
||||||
width: double.infinity,
|
height: constraints.maxHeight,
|
||||||
height: double.infinity,
|
fit: BoxFit.cover,
|
||||||
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,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: StreamAttachmentUploadStateBuilder(
|
child: StreamAttachmentUploadStateBuilder(
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: StreamImageAttachment(
|
child: StreamImageAttachment(
|
||||||
|
shape: shape,
|
||||||
message: message,
|
message: message,
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
image: image,
|
image: image,
|
||||||
|
|||||||
@@ -3,67 +3,71 @@ part of 'attachment_widget_builder.dart';
|
|||||||
/// {@template mixedAttachmentBuilder}
|
/// {@template mixedAttachmentBuilder}
|
||||||
/// A widget builder for Mixed attachment type.
|
/// A widget builder for Mixed attachment type.
|
||||||
///
|
///
|
||||||
/// This builder is used when a message contains both image/video/giphy and file
|
/// This builder is used when a message contains a mix of media type and file
|
||||||
/// attachments.
|
/// or url preview attachments.
|
||||||
///
|
///
|
||||||
/// This builder will render first image/video/giphy attachment and then render
|
/// This builder will render first the url preview or file attachment and then
|
||||||
/// the file attachments.
|
/// the media attachments.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
||||||
/// {@macro mixedAttachmentBuilder}
|
/// {@macro mixedAttachmentBuilder}
|
||||||
MixedAttachmentBuilder({
|
MixedAttachmentBuilder({
|
||||||
this.shape,
|
|
||||||
this.padding = const EdgeInsets.all(4),
|
this.padding = const EdgeInsets.all(4),
|
||||||
this.onAttachmentTap,
|
StreamAttachmentWidgetTapCallback? onAttachmentTap,
|
||||||
}) : _imageAttachmentBuilder = ImageAttachmentBuilder(
|
}) : _imageAttachmentBuilder = ImageAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
|
|
||||||
),
|
),
|
||||||
_videoAttachmentBuilder = VideoAttachmentBuilder(
|
_videoAttachmentBuilder = VideoAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
|
|
||||||
),
|
),
|
||||||
_giphyAttachmentBuilder = GiphyAttachmentBuilder(
|
_giphyAttachmentBuilder = GiphyAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
|
|
||||||
),
|
),
|
||||||
_galleryAttachmentBuilder = GalleryAttachmentBuilder(
|
_galleryAttachmentBuilder = GalleryAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
|
|
||||||
),
|
),
|
||||||
_fileAttachmentBuilder = FileAttachmentBuilder(
|
_fileAttachmentBuilder = FileAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onAttachmentTap: onAttachmentTap,
|
||||||
|
),
|
||||||
|
_urlAttachmentBuilder = UrlAttachmentBuilder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
padding: EdgeInsets.symmetric(horizontal: padding.horizontal),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The shape of the gallery attachment.
|
/// The padding to apply to the mixed attachment widget.
|
||||||
final ShapeBorder? shape;
|
|
||||||
|
|
||||||
/// The padding to apply to the gallery attachment widget.
|
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
/// The callback to call when the attachment is tapped.
|
|
||||||
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
|
||||||
|
|
||||||
late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder;
|
late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder;
|
||||||
late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder;
|
late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder;
|
||||||
late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder;
|
late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder;
|
||||||
late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder;
|
late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder;
|
||||||
late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder;
|
late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder;
|
||||||
|
late final StreamAttachmentWidgetBuilder _urlAttachmentBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool canHandle(
|
bool canHandle(
|
||||||
Message message,
|
Message message,
|
||||||
Map<String, List<Attachment>> attachments,
|
Map<String, List<Attachment>> attachments,
|
||||||
) {
|
) {
|
||||||
final containsImage = attachments.keys.contains(AttachmentType.image);
|
final types = attachments.keys;
|
||||||
final containsVideo = attachments.keys.contains(AttachmentType.video);
|
|
||||||
final containsGiphy = attachments.keys.contains(AttachmentType.giphy);
|
final containsImage = types.contains(AttachmentType.image);
|
||||||
final containsFile = attachments.keys.contains(AttachmentType.file);
|
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;
|
final containsMedia = containsImage || containsVideo || containsGiphy;
|
||||||
|
|
||||||
return containsMedia && containsFile;
|
return containsMedia && containsFile ||
|
||||||
|
containsMedia && containsUrlPreview ||
|
||||||
|
containsFile && containsUrlPreview ||
|
||||||
|
containsMedia && containsFile && containsUrlPreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -74,6 +78,7 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
|||||||
) {
|
) {
|
||||||
assert(debugAssertCanHandle(message, attachments), '');
|
assert(debugAssertCanHandle(message, attachments), '');
|
||||||
|
|
||||||
|
final urls = attachments[AttachmentType.urlPreview];
|
||||||
final files = attachments[AttachmentType.file];
|
final files = attachments[AttachmentType.file];
|
||||||
final images = attachments[AttachmentType.image];
|
final images = attachments[AttachmentType.image];
|
||||||
final videos = attachments[AttachmentType.video];
|
final videos = attachments[AttachmentType.video];
|
||||||
@@ -86,11 +91,14 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (urls != null)
|
||||||
|
_urlAttachmentBuilder.build(context, message, {
|
||||||
|
AttachmentType.urlPreview: urls,
|
||||||
|
}),
|
||||||
if (files != null)
|
if (files != null)
|
||||||
for (final file in files)
|
_fileAttachmentBuilder.build(context, message, {
|
||||||
_fileAttachmentBuilder.build(context, message, {
|
AttachmentType.file: files,
|
||||||
AttachmentType.file: [file],
|
}),
|
||||||
}),
|
|
||||||
if (shouldBuildGallery)
|
if (shouldBuildGallery)
|
||||||
_galleryAttachmentBuilder.build(context, message, {
|
_galleryAttachmentBuilder.build(context, message, {
|
||||||
if (images != null) AttachmentType.image: images,
|
if (images != null) AttachmentType.image: images,
|
||||||
|
|||||||
@@ -128,19 +128,21 @@ class _FileTypeImage extends StatelessWidget {
|
|||||||
file: file,
|
file: file,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
// fit: BoxFit.cover,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final mimeType = file.title?.mimeType?.type;
|
final mediaType = file.title?.mediaType;
|
||||||
final isImage = mimeType == 'image';
|
final isImage = mediaType?.type == AttachmentType.image;
|
||||||
final isVideo = mimeType == 'video';
|
final isVideo = mediaType?.type == AttachmentType.video;
|
||||||
if (isImage || isVideo) {
|
if (isImage || isVideo) {
|
||||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||||
child = Container(
|
child = Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: ShapeDecoration(
|
decoration: ShapeDecoration(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(color: colorTheme.borders),
|
side: BorderSide(
|
||||||
|
color: colorTheme.borders,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ class StreamGalleryAttachment extends StatelessWidget {
|
|||||||
[1],
|
[1],
|
||||||
[1],
|
[1],
|
||||||
],
|
],
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
children: [
|
children: [
|
||||||
itemBuilder(context, 0),
|
itemBuilder(context, 0),
|
||||||
itemBuilder(context, 1),
|
itemBuilder(context, 1),
|
||||||
@@ -145,6 +147,8 @@ class StreamGalleryAttachment extends StatelessWidget {
|
|||||||
pattern: const [
|
pattern: const [
|
||||||
[1, 1],
|
[1, 1],
|
||||||
],
|
],
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
children: [
|
children: [
|
||||||
itemBuilder(context, 0),
|
itemBuilder(context, 0),
|
||||||
itemBuilder(context, 1),
|
itemBuilder(context, 1),
|
||||||
@@ -170,6 +174,8 @@ class StreamGalleryAttachment extends StatelessWidget {
|
|||||||
pattern: [
|
pattern: [
|
||||||
if (isLandscape1) [2, 1] else [1, 2],
|
if (isLandscape1) [2, 1] else [1, 2],
|
||||||
],
|
],
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
children: [
|
children: [
|
||||||
itemBuilder(context, 0),
|
itemBuilder(context, 0),
|
||||||
itemBuilder(context, 1),
|
itemBuilder(context, 1),
|
||||||
@@ -204,6 +210,8 @@ class StreamGalleryAttachment extends StatelessWidget {
|
|||||||
[1],
|
[1],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
],
|
],
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
reverse: !isLandscape1,
|
reverse: !isLandscape1,
|
||||||
children: [
|
children: [
|
||||||
itemBuilder(context, 0),
|
itemBuilder(context, 0),
|
||||||
@@ -238,6 +246,8 @@ class StreamGalleryAttachment extends StatelessWidget {
|
|||||||
return FlexGrid(
|
return FlexGrid(
|
||||||
pattern: pattern,
|
pattern: pattern,
|
||||||
maxChildren: 4,
|
maxChildren: 4,
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
children: children,
|
children: children,
|
||||||
overlayBuilder: (context, remaining) {
|
overlayBuilder: (context, remaining) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class StreamGiphyAttachment extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.giphy,
|
required this.giphy,
|
||||||
this.type = GiphyInfoType.fixedHeightDownsampled,
|
this.type = GiphyInfoType.original,
|
||||||
this.shape,
|
this.shape,
|
||||||
this.constraints = const BoxConstraints(),
|
this.constraints = const BoxConstraints(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,18 +50,18 @@ Future<AttachmentData> downloadAttachmentData(
|
|||||||
String? downloadUrl;
|
String? downloadUrl;
|
||||||
String? fileName;
|
String? fileName;
|
||||||
/* ---IMAGES/GIFS--- */
|
/* ---IMAGES/GIFS--- */
|
||||||
if (type == 'image') {
|
if (type == AttachmentType.image) {
|
||||||
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
|
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
|
||||||
fileName = attachment.title;
|
fileName = attachment.title;
|
||||||
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
|
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
|
||||||
}
|
}
|
||||||
/* ---GIPHY's--- */
|
/* ---GIPHY's--- */
|
||||||
else if (type == 'giphy') {
|
else if (type == AttachmentType.giphy) {
|
||||||
downloadUrl = attachment.thumbUrl;
|
downloadUrl = attachment.thumbUrl;
|
||||||
fileName = '${attachment.title}.gif';
|
fileName = '${attachment.title}.gif';
|
||||||
}
|
}
|
||||||
/* ---FILES AND VIDEOS--- */
|
/* ---FILES AND VIDEOS--- */
|
||||||
else if (type == 'file' || type == 'video') {
|
else if (type == AttachmentType.file || type == AttachmentType.video) {
|
||||||
downloadUrl = attachment.assetUrl;
|
downloadUrl = attachment.assetUrl;
|
||||||
fileName = attachment.title;
|
fileName = attachment.title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
if (isImage) {
|
||||||
return StreamImageAttachmentThumbnail(
|
return StreamImageAttachmentThumbnail(
|
||||||
image: file,
|
image: file,
|
||||||
@@ -62,7 +62,7 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isVideo = mimeType == 'video';
|
final isVideo = mediaType?.type == AttachmentType.video;
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
return StreamVideoAttachmentThumbnail(
|
return StreamVideoAttachmentThumbnail(
|
||||||
video: file,
|
video: file,
|
||||||
@@ -73,6 +73,6 @@ class StreamFileAttachmentThumbnail extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return a generic file type icon.
|
// Return a generic file type icon.
|
||||||
return getFileTypeImage(mimeType);
|
return getFileTypeImage(mediaType?.mimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget {
|
|||||||
return ThumbnailError(
|
return ThumbnailError(
|
||||||
error: error,
|
error: error,
|
||||||
stackTrace: stackTrace,
|
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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.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:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.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/theme/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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}
|
/// {@template imageAttachmentThumbnail}
|
||||||
/// Widget for building image attachment thumbnail.
|
/// Widget for building image attachment thumbnail.
|
||||||
///
|
///
|
||||||
@@ -101,6 +64,9 @@ class StreamImageAttachmentThumbnail extends StatelessWidget {
|
|||||||
return ThumbnailError(
|
return ThumbnailError(
|
||||||
error: error,
|
error: error,
|
||||||
stackTrace: stackTrace,
|
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,
|
super.key,
|
||||||
required this.error,
|
required this.error,
|
||||||
this.stackTrace,
|
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.
|
/// The error that triggered this error widget.
|
||||||
final Object error;
|
final Object error;
|
||||||
|
|
||||||
@@ -33,7 +45,9 @@ class ThumbnailError extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
fit: BoxFit.cover,
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
package: 'stream_chat_flutter',
|
package: 'stream_chat_flutter',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget {
|
|||||||
return ThumbnailError(
|
return ThumbnailError(
|
||||||
error: error,
|
error: error,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
|
height: double.infinity,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
color: colorTheme.borders,
|
color: colorTheme.borders,
|
||||||
strokeAlign: BorderSide.strokeAlignOutside,
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(8),
|
||||||
);
|
);
|
||||||
|
|
||||||
final backgroundColor = messageTheme.urlAttachmentBackgroundColor;
|
final backgroundColor = messageTheme.urlAttachmentBackgroundColor;
|
||||||
@@ -62,44 +62,43 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (urlAttachment.imageUrl != null)
|
Stack(
|
||||||
Stack(
|
children: [
|
||||||
children: [
|
AspectRatio(
|
||||||
AspectRatio(
|
// Default aspect ratio for Open Graph images.
|
||||||
// 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
|
||||||
// https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage
|
aspectRatio: 1.91 / 1,
|
||||||
aspectRatio: 1.91 / 1,
|
child: StreamImageAttachmentThumbnail(
|
||||||
child: StreamImageAttachmentThumbnail(
|
image: urlAttachment,
|
||||||
image: urlAttachment,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
left: 0,
|
Positioned(
|
||||||
bottom: 0,
|
left: 0,
|
||||||
child: DecoratedBox(
|
bottom: 0,
|
||||||
decoration: BoxDecoration(
|
child: DecoratedBox(
|
||||||
borderRadius: const BorderRadius.only(
|
decoration: BoxDecoration(
|
||||||
topRight: Radius.circular(16),
|
borderRadius: const BorderRadius.only(
|
||||||
),
|
topRight: Radius.circular(16),
|
||||||
color: backgroundColor,
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
color: backgroundColor,
|
||||||
padding: const EdgeInsets.only(
|
),
|
||||||
top: 8,
|
child: Padding(
|
||||||
left: 8,
|
padding: const EdgeInsets.only(
|
||||||
right: 12,
|
top: 8,
|
||||||
bottom: 4,
|
left: 8,
|
||||||
),
|
right: 12,
|
||||||
child: Text(
|
bottom: 4,
|
||||||
hostDisplayName,
|
),
|
||||||
style: messageTheme.urlAttachmentHostStyle,
|
child: Text(
|
||||||
),
|
hostDisplayName,
|
||||||
|
style: messageTheme.urlAttachmentHostStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
if (showSave)
|
if (showSave)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
attachment.type == 'video'
|
attachment.type == AttachmentType.video
|
||||||
? context.translations.saveVideoLabel
|
? context.translations.saveVideoLabel
|
||||||
: context.translations.saveImageLabel,
|
: context.translations.saveImageLabel,
|
||||||
StreamSvgIcon.iconSave(
|
StreamSvgIcon.iconSave(
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ class StreamMessagePreviewText extends StatelessWidget {
|
|||||||
|
|
||||||
final messageTextParts = [
|
final messageTextParts = [
|
||||||
...messageAttachments.map((it) {
|
...messageAttachments.map((it) {
|
||||||
if (it.type == 'image') {
|
if (it.type == AttachmentType.image) {
|
||||||
return '📷';
|
return '📷';
|
||||||
} else if (it.type == 'video') {
|
} else if (it.type == AttachmentType.video) {
|
||||||
return '🎬';
|
return '🎬';
|
||||||
} else if (it.type == 'giphy') {
|
} else if (it.type == AttachmentType.giphy) {
|
||||||
return '[GIF]';
|
return '[GIF]';
|
||||||
}
|
}
|
||||||
return it == message.attachments.last
|
return it == message.attachments.last
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
|
||||||
import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart';
|
import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart';
|
||||||
import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart';
|
import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
@@ -272,7 +272,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (widget.autoplayVideos &&
|
if (widget.autoplayVideos &&
|
||||||
currentAttachment.type == 'video') {
|
currentAttachment.type == AttachmentType.video) {
|
||||||
final controller = videoPackages[currentAttachment.id]!;
|
final controller = videoPackages[currentAttachment.id]!;
|
||||||
controller._chewieController?.play();
|
controller._chewieController?.play();
|
||||||
}
|
}
|
||||||
@@ -294,47 +294,19 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
|
|||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
verticalPadding: 0,
|
verticalPadding: 0,
|
||||||
builder: (_) => [
|
builder: (_) => [
|
||||||
DownloadMenuItem(
|
DownloadMenuItem(attachment: attachment),
|
||||||
attachment: attachment,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: PhotoView.customChild(
|
child: PhotoView.customChild(
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: attachment.id,
|
|
||||||
),
|
|
||||||
// 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,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
child: StreamImageAttachmentThumbnail(
|
|
||||||
image: attachment,
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
|
||||||
maxScale: PhotoViewComputedScale.covered,
|
maxScale: PhotoViewComputedScale.covered,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
// heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
// tag: widget.mediaAttachmentPackages,
|
|
||||||
// ),
|
|
||||||
backgroundDecoration: const BoxDecoration(
|
backgroundDecoration: const BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
),
|
),
|
||||||
|
child: StreamMediaAttachmentThumbnail(
|
||||||
|
media: attachment,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -353,9 +325,7 @@ class _FullScreenMediaState extends State<StreamFullScreenMedia> {
|
|||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
verticalPadding: 0,
|
verticalPadding: 0,
|
||||||
builder: (_) => [
|
builder: (_) => [
|
||||||
DownloadMenuItem(
|
DownloadMenuItem(attachment: attachment),
|
||||||
attachment: attachment,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: controller.chewieController!,
|
controller: controller.chewieController!,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
|
|||||||
_pageController = PageController(initialPage: widget.startIndex);
|
_pageController = PageController(initialPage: widget.startIndex);
|
||||||
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
|
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
|
||||||
final attachment = widget.mediaAttachmentPackages[i].attachment;
|
final attachment = widget.mediaAttachmentPackages[i].attachment;
|
||||||
if (attachment.type != 'video') continue;
|
if (attachment.type != AttachmentType.video) continue;
|
||||||
final package = DesktopVideoPackage(attachment);
|
final package = DesktopVideoPackage(attachment);
|
||||||
videoPackages[attachment.id] = package;
|
videoPackages[attachment.id] = package;
|
||||||
}
|
}
|
||||||
@@ -298,7 +298,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
|
|||||||
p.player.pause();
|
p.player.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (widget.autoplayVideos && currentAttachment.type == 'video') {
|
if (widget.autoplayVideos &&
|
||||||
|
currentAttachment.type == AttachmentType.video) {
|
||||||
final package = videoPackages[currentAttachment.id]!;
|
final package = videoPackages[currentAttachment.id]!;
|
||||||
package.player.play();
|
package.player.play();
|
||||||
}
|
}
|
||||||
@@ -307,7 +308,8 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
|
|||||||
final currentAttachmentPackage =
|
final currentAttachmentPackage =
|
||||||
widget.mediaAttachmentPackages[index];
|
widget.mediaAttachmentPackages[index];
|
||||||
final attachment = currentAttachmentPackage.attachment;
|
final attachment = currentAttachmentPackage.attachment;
|
||||||
if (attachment.type == 'image' || attachment.type == 'giphy') {
|
if (attachment.type == AttachmentType.image ||
|
||||||
|
attachment.type == AttachmentType.giphy) {
|
||||||
final imageUrl = attachment.imageUrl ??
|
final imageUrl = attachment.imageUrl ??
|
||||||
attachment.assetUrl ??
|
attachment.assetUrl ??
|
||||||
attachment.thumbUrl;
|
attachment.thumbUrl;
|
||||||
@@ -359,7 +361,7 @@ class _FullScreenMediaDesktopState extends State<FullScreenMediaDesktop> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (attachment.type == 'video') {
|
} else if (attachment.type == AttachmentType.video) {
|
||||||
final package = videoPackages[attachment.id]!;
|
final package = videoPackages[attachment.id]!;
|
||||||
package.player.open(
|
package.player.open(
|
||||||
Playlist(
|
Playlist(
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
|
|||||||
final url = attachment.imageUrl ??
|
final url = attachment.imageUrl ??
|
||||||
attachment.assetUrl ??
|
attachment.assetUrl ??
|
||||||
attachment.thumbUrl!;
|
attachment.thumbUrl!;
|
||||||
final type = attachment.type == 'image'
|
final type = attachment.type == AttachmentType.image
|
||||||
? 'jpg'
|
? 'jpg'
|
||||||
: url.split('?').first.split('.').last;
|
: url.split('?').first.split('.').last;
|
||||||
final request = await HttpClient().getUrl(Uri.parse(url));
|
final request = await HttpClient().getUrl(Uri.parse(url));
|
||||||
@@ -218,7 +218,7 @@ class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
|
|||||||
widget.mediaAttachmentPackages[index];
|
widget.mediaAttachmentPackages[index];
|
||||||
final attachment = attachmentPackage.attachment;
|
final attachment = attachmentPackage.attachment;
|
||||||
final message = attachmentPackage.message;
|
final message = attachmentPackage.message;
|
||||||
if (attachment.type == 'video') {
|
if (attachment.type == AttachmentType.video) {
|
||||||
media = MouseRegion(
|
media = MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
|||||||
@@ -114,119 +114,121 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
|
|
||||||
final child = Center(
|
final child = Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: SafeArea(
|
||||||
padding: const EdgeInsets.all(8),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(8),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
if (widget.showReactionPicker && hasReactionPermission)
|
children: <Widget>[
|
||||||
LayoutBuilder(
|
if (widget.showReactionPicker && hasReactionPermission)
|
||||||
builder: (context, constraints) {
|
LayoutBuilder(
|
||||||
return Align(
|
builder: (context, constraints) {
|
||||||
alignment: Alignment(
|
return Align(
|
||||||
calculateReactionsHorizontalAlignment(
|
alignment: Alignment(
|
||||||
user,
|
calculateReactionsHorizontalAlignment(
|
||||||
widget.message,
|
user,
|
||||||
constraints,
|
widget.message,
|
||||||
fontSize,
|
constraints,
|
||||||
orientation,
|
fontSize,
|
||||||
|
orientation,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0,
|
child: StreamReactionPicker(
|
||||||
),
|
message: widget.message,
|
||||||
child: StreamReactionPicker(
|
),
|
||||||
message: widget.message,
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
const SizedBox(height: 10),
|
||||||
|
IgnorePointer(
|
||||||
|
child: widget.messageWidget,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
IgnorePointer(
|
Padding(
|
||||||
child: widget.messageWidget,
|
padding: EdgeInsets.only(
|
||||||
),
|
left: widget.reverse ? 0 : 40,
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Padding(
|
child: SizedBox(
|
||||||
padding: EdgeInsets.only(
|
width: mediaQueryData.size.width * 0.75,
|
||||||
left: widget.reverse ? 0 : 40,
|
child: Material(
|
||||||
),
|
color: streamChatThemeData.colorTheme.appBg,
|
||||||
child: SizedBox(
|
clipBehavior: Clip.hardEdge,
|
||||||
width: mediaQueryData.size.width * 0.75,
|
shape: RoundedRectangleBorder(
|
||||||
child: Material(
|
borderRadius: BorderRadius.circular(16),
|
||||||
color: streamChatThemeData.colorTheme.appBg,
|
),
|
||||||
clipBehavior: Clip.hardEdge,
|
child: Column(
|
||||||
shape: RoundedRectangleBorder(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
borderRadius: BorderRadius.circular(16),
|
children: [
|
||||||
),
|
if (widget.showReplyMessage &&
|
||||||
child: Column(
|
widget.message.state.isCompleted)
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
ReplyButton(
|
||||||
children: [
|
onTap: () {
|
||||||
if (widget.showReplyMessage &&
|
Navigator.of(context).pop();
|
||||||
widget.message.state.isCompleted)
|
if (widget.onReplyTap != null) {
|
||||||
ReplyButton(
|
widget.onReplyTap?.call(widget.message);
|
||||||
onTap: () {
|
}
|
||||||
Navigator.of(context).pop();
|
},
|
||||||
if (widget.onReplyTap != null) {
|
),
|
||||||
widget.onReplyTap?.call(widget.message);
|
if (widget.showThreadReplyMessage &&
|
||||||
}
|
(widget.message.state.isCompleted) &&
|
||||||
},
|
widget.message.parentId == null)
|
||||||
|
ThreadReplyButton(
|
||||||
|
message: widget.message,
|
||||||
|
onThreadReplyTap: widget.onThreadReplyTap,
|
||||||
|
),
|
||||||
|
if (widget.showResendMessage)
|
||||||
|
ResendMessageButton(
|
||||||
|
message: widget.message,
|
||||||
|
channel: channel,
|
||||||
|
),
|
||||||
|
if (widget.showEditMessage)
|
||||||
|
EditMessageButton(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_showEditBottomSheet(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.showCopyMessage)
|
||||||
|
CopyMessageButton(
|
||||||
|
onTap: () {
|
||||||
|
widget.onCopyTap?.call(widget.message);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.showFlagButton)
|
||||||
|
FlagMessageButton(
|
||||||
|
onTap: _showFlagDialog,
|
||||||
|
),
|
||||||
|
if (widget.showPinButton)
|
||||||
|
PinMessageButton(
|
||||||
|
onTap: _togglePin,
|
||||||
|
pinned: widget.message.pinned,
|
||||||
|
),
|
||||||
|
if (widget.showDeleteMessage)
|
||||||
|
DeleteMessageButton(
|
||||||
|
isDeleteFailed:
|
||||||
|
widget.message.state.isDeletingFailed,
|
||||||
|
onTap: _showDeleteBottomSheet,
|
||||||
|
),
|
||||||
|
...widget.customActions
|
||||||
|
.map((action) => _buildCustomAction(
|
||||||
|
context,
|
||||||
|
action,
|
||||||
|
)),
|
||||||
|
].insertBetween(
|
||||||
|
Container(
|
||||||
|
height: 1,
|
||||||
|
color: streamChatThemeData.colorTheme.borders,
|
||||||
),
|
),
|
||||||
if (widget.showThreadReplyMessage &&
|
|
||||||
(widget.message.state.isCompleted) &&
|
|
||||||
widget.message.parentId == null)
|
|
||||||
ThreadReplyButton(
|
|
||||||
message: widget.message,
|
|
||||||
onThreadReplyTap: widget.onThreadReplyTap,
|
|
||||||
),
|
|
||||||
if (widget.showResendMessage)
|
|
||||||
ResendMessageButton(
|
|
||||||
message: widget.message,
|
|
||||||
channel: channel,
|
|
||||||
),
|
|
||||||
if (widget.showEditMessage)
|
|
||||||
EditMessageButton(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_showEditBottomSheet(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (widget.showCopyMessage)
|
|
||||||
CopyMessageButton(
|
|
||||||
onTap: () {
|
|
||||||
widget.onCopyTap?.call(widget.message);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (widget.showFlagButton)
|
|
||||||
FlagMessageButton(
|
|
||||||
onTap: _showFlagDialog,
|
|
||||||
),
|
|
||||||
if (widget.showPinButton)
|
|
||||||
PinMessageButton(
|
|
||||||
onTap: _togglePin,
|
|
||||||
pinned: widget.message.pinned,
|
|
||||||
),
|
|
||||||
if (widget.showDeleteMessage)
|
|
||||||
DeleteMessageButton(
|
|
||||||
isDeleteFailed:
|
|
||||||
widget.message.state.isDeletingFailed,
|
|
||||||
onTap: _showDeleteBottomSheet,
|
|
||||||
),
|
|
||||||
...widget.customActions
|
|
||||||
.map((action) => _buildCustomAction(
|
|
||||||
context,
|
|
||||||
action,
|
|
||||||
)),
|
|
||||||
].insertBetween(
|
|
||||||
Container(
|
|
||||||
height: 1,
|
|
||||||
color: streamChatThemeData.colorTheme.borders,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ extension StreamImagePickerX on StreamAttachmentPickerController {
|
|||||||
|
|
||||||
final extraDataMap = <String, Object>{};
|
final extraDataMap = <String, Object>{};
|
||||||
|
|
||||||
final mimeType = file.mimeType?.mimeType;
|
final mimeType = file.mediaType?.mimeType;
|
||||||
|
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
extraDataMap['mime_type'] = mimeType;
|
extraDataMap['mime_type'] = mimeType;
|
||||||
|
|||||||
@@ -240,10 +240,10 @@ class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption {
|
|||||||
extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController {
|
extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController {
|
||||||
/// Returns the list of available attachment picker options.
|
/// Returns the list of available attachment picker options.
|
||||||
Set<AttachmentPickerType> get currentAttachmentPickerTypes {
|
Set<AttachmentPickerType> get currentAttachmentPickerTypes {
|
||||||
final containsImage = value.any((it) => it.type == 'image');
|
final containsImage = value.any((it) => it.type == AttachmentType.image);
|
||||||
final containsVideo = value.any((it) => it.type == 'video');
|
final containsVideo = value.any((it) => it.type == AttachmentType.video);
|
||||||
final containsAudio = value.any((it) => it.type == 'audio');
|
final containsAudio = value.any((it) => it.type == AttachmentType.audio);
|
||||||
final containsFile = value.any((it) => it.type == 'file');
|
final containsFile = value.any((it) => it.type == AttachmentType.file);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
if (containsImage) AttachmentPickerType.images,
|
if (containsImage) AttachmentPickerType.images,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/file_attachment_thumbnail.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
typedef _Builders = Map<String, QuotedMessageAttachmentThumbnailBuilder>;
|
||||||
|
|
||||||
/// {@template streamQuotedMessage}
|
/// {@template streamQuotedMessage}
|
||||||
/// Widget for the quoted message.
|
/// Widget for the quoted message.
|
||||||
@@ -40,8 +41,7 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
final int textLimit;
|
final int textLimit;
|
||||||
|
|
||||||
/// Map that defines a thumbnail builder for an attachment type
|
/// Map that defines a thumbnail builder for an attachment type
|
||||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
final _Builders? attachmentThumbnailBuilders;
|
||||||
attachmentThumbnailBuilders;
|
|
||||||
|
|
||||||
/// Padding around the widget
|
/// Padding around the widget
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
@@ -109,19 +109,17 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
final bool reverse;
|
final bool reverse;
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
/// Map that defines a thumbnail builder for an attachment type
|
final _Builders? attachmentThumbnailBuilders;
|
||||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
|
||||||
attachmentThumbnailBuilders;
|
|
||||||
|
|
||||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||||
|
|
||||||
bool get _containsText => message.text?.isNotEmpty == true;
|
bool get _containsText => message.text?.isNotEmpty == true;
|
||||||
|
|
||||||
bool get _containsLinkAttachment =>
|
bool get _containsLinkAttachment =>
|
||||||
message.attachments.any((element) => element.titleLink != null);
|
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
|
||||||
|
|
||||||
bool get _isGiphy =>
|
bool get _isGiphy => message.attachments
|
||||||
message.attachments.any((element) => element.type == 'giphy');
|
.any((element) => element.type == AttachmentType.giphy);
|
||||||
|
|
||||||
bool get _isDeleted => message.isDeleted || message.deletedAt != null;
|
bool get _isDeleted => message.isDeleted || message.deletedAt != null;
|
||||||
|
|
||||||
@@ -150,14 +148,6 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
// Show quoted message
|
// Show quoted message
|
||||||
children = [
|
children = [
|
||||||
if (onQuotedMessageClear != null)
|
|
||||||
PlatformWidgetBuilder(
|
|
||||||
web: (context, child) => child,
|
|
||||||
desktop: (context, child) => child,
|
|
||||||
child: ClearInputItemButton(
|
|
||||||
onTap: onQuotedMessageClear,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_hasAttachments)
|
if (_hasAttachments)
|
||||||
_ParseAttachments(
|
_ParseAttachments(
|
||||||
message: message,
|
message: message,
|
||||||
@@ -184,9 +174,26 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
].insertBetween(const SizedBox(width: 8));
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add clear button if needed.
|
||||||
|
if (onQuotedMessageClear != null) {
|
||||||
|
children.insert(
|
||||||
|
0,
|
||||||
|
PlatformWidgetBuilder(
|
||||||
|
web: (context, child) => child,
|
||||||
|
desktop: (context, child) => child,
|
||||||
|
child: ClearInputItemButton(
|
||||||
|
onTap: onQuotedMessageClear,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some spacing between the children.
|
||||||
|
children = children.insertBetween(const SizedBox(width: 8));
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getBackgroundColor(context),
|
color: _getBackgroundColor(context),
|
||||||
@@ -229,193 +236,106 @@ class _ParseAttachments extends StatelessWidget {
|
|||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
final StreamMessageThemeData messageTheme;
|
final StreamMessageThemeData messageTheme;
|
||||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
final _Builders? attachmentThumbnailBuilders;
|
||||||
attachmentThumbnailBuilders;
|
|
||||||
|
|
||||||
bool get _containsLinkAttachment =>
|
|
||||||
message.attachments.any((element) => element.titleLink != null);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child;
|
final attachment = message.attachments.first;
|
||||||
Attachment attachment;
|
|
||||||
if (_containsLinkAttachment) {
|
var attachmentBuilders = attachmentThumbnailBuilders;
|
||||||
attachment = message.attachments.firstWhere(
|
attachmentBuilders ??= _createDefaultAttachmentBuilders();
|
||||||
(element) => element.ogScrapeUrl != null || element.titleLink != null,
|
|
||||||
|
// Build the attachment widget using the builder for the attachment type.
|
||||||
|
final attachmentWidget = attachmentBuilders[attachment.type]?.call(
|
||||||
|
context,
|
||||||
|
attachment,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return empty container if no attachment widget is returned.
|
||||||
|
if (attachmentWidget == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||||
|
|
||||||
|
var clipBehavior = Clip.none;
|
||||||
|
ShapeDecoration? decoration;
|
||||||
|
if (attachment.type != AttachmentType.file) {
|
||||||
|
clipBehavior = Clip.hardEdge;
|
||||||
|
decoration = ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorTheme.borders,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
child = _UrlAttachment(attachment: attachment);
|
|
||||||
} else {
|
|
||||||
QuotedMessageAttachmentThumbnailBuilder? attachmentBuilder;
|
|
||||||
attachment = message.attachments.last;
|
|
||||||
if (attachmentThumbnailBuilders?.containsKey(attachment.type) == true) {
|
|
||||||
attachmentBuilder = attachmentThumbnailBuilders![attachment.type];
|
|
||||||
}
|
|
||||||
attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
|
|
||||||
if (attachmentBuilder == null) {
|
|
||||||
child = const Offstage();
|
|
||||||
} else {
|
|
||||||
child = attachmentBuilder(context, attachment);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final isImageFile = attachment.title?.mimeType?.type == 'image';
|
return Container(
|
||||||
final isVideoFile = attachment.title?.mimeType?.type == 'video';
|
key: Key(attachment.id),
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
decoration: decoration,
|
||||||
|
constraints: const BoxConstraints.tightFor(width: 36, height: 36),
|
||||||
|
child: AbsorbPointer(child: attachmentWidget),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Material(
|
_Builders _createDefaultAttachmentBuilders() {
|
||||||
clipBehavior: Clip.hardEdge,
|
Widget _createMediaThumbnail(BuildContext context, Attachment media) {
|
||||||
type: MaterialType.transparency,
|
return StreamImageAttachmentThumbnail(
|
||||||
shape: attachment.type == 'file' && (!isImageFile && !isVideoFile)
|
image: media,
|
||||||
? null
|
width: double.infinity,
|
||||||
: RoundedRectangleBorder(
|
height: double.infinity,
|
||||||
side: const BorderSide(width: 0, color: Colors.transparent),
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _createUrlThumbnail(BuildContext context, Attachment media) {
|
||||||
|
return StreamImageAttachmentThumbnail(
|
||||||
|
image: media,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _createFileThumbnail(BuildContext context, Attachment file) {
|
||||||
|
Widget thumbnail = StreamFileAttachmentThumbnail(
|
||||||
|
file: file,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
thumbnail = Container(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorTheme.borders,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: AbsorbPointer(child: child),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
|
||||||
get _defaultAttachmentBuilder {
|
|
||||||
final builders = <String, QuotedMessageAttachmentThumbnailBuilder>{
|
|
||||||
'image': (_, attachment) {
|
|
||||||
return StreamImageAttachment(
|
|
||||||
message: message,
|
|
||||||
image: attachment,
|
|
||||||
constraints: BoxConstraints.loose(const Size(32, 32)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
'video': (_, attachment) {
|
|
||||||
return StreamVideoAttachmentThumbnail(
|
|
||||||
key: ValueKey(attachment.assetUrl),
|
|
||||||
video: attachment,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
// constraints: BoxConstraints.loose(const Size(32, 32)),
|
|
||||||
// errorBuilder: (_, __) => AttachmentError(
|
|
||||||
// constraints: BoxConstraints.loose(const Size(32, 32)),
|
|
||||||
// ),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
'giphy': (_, attachment) {
|
|
||||||
const size = Size(32, 32);
|
|
||||||
return CachedNetworkImage(
|
|
||||||
height: size.height,
|
|
||||||
width: size.width,
|
|
||||||
placeholder: (_, __) {
|
|
||||||
return SizedBox(
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
imageUrl: attachment.thumbUrl ??
|
|
||||||
attachment.imageUrl ??
|
|
||||||
attachment.assetUrl!,
|
|
||||||
errorWidget: (context, url, error) =>
|
|
||||||
AttachmentError(constraints: BoxConstraints.loose(size)),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
builders['file'] = (_, attachment) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final isImageFile = attachment.title?.mimeType?.type == 'image';
|
|
||||||
if (isImageFile) {
|
|
||||||
return builders['image']!(context, attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
final isVideoFile = attachment.title?.mimeType?.type == 'video';
|
|
||||||
if (isVideoFile) {
|
|
||||||
return builders['video']!(context, attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getFileTypeImage(
|
|
||||||
attachment.extraData['mime_type'] as String?,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return builders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _UrlAttachment extends StatelessWidget {
|
|
||||||
const _UrlAttachment({
|
|
||||||
required this.attachment,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Attachment attachment;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const size = Size(32, 32);
|
|
||||||
if (attachment.thumbUrl != null) {
|
|
||||||
return Container(
|
|
||||||
height: size.height,
|
|
||||||
width: size.width,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: CachedNetworkImageProvider(
|
|
||||||
attachment.thumbUrl!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
child: thumbnail,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
}
|
}
|
||||||
return AttachmentError(constraints: BoxConstraints.loose(size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
return {
|
||||||
const _VideoAttachmentThumbnail({
|
AttachmentType.image: _createMediaThumbnail,
|
||||||
required this.attachment,
|
AttachmentType.giphy: _createMediaThumbnail,
|
||||||
});
|
AttachmentType.video: _createMediaThumbnail,
|
||||||
|
AttachmentType.urlPreview: _createUrlThumbnail,
|
||||||
final Attachment attachment;
|
AttachmentType.file: _createFileThumbnail,
|
||||||
|
};
|
||||||
@override
|
|
||||||
_VideoAttachmentThumbnailState createState() =>
|
|
||||||
_VideoAttachmentThumbnailState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
|
||||||
late VideoPlayerController _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = VideoPlayerController.networkUrl(
|
|
||||||
Uri.parse(widget.attachment.assetUrl!),
|
|
||||||
)..initialize().then((_) {
|
|
||||||
// ignore: no-empty-block
|
|
||||||
setState(() {}); //when your thumbnail will show.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: _controller.value.isInitialized
|
|
||||||
? VideoPlayer(_controller)
|
|
||||||
: const CircularProgressIndicator.adaptive(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1169,7 +1169,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final containsUrl = quotedMessage.attachments.any((it) {
|
final containsUrl = quotedMessage.attachments.any((it) {
|
||||||
return it.titleLink != null;
|
return it.type == AttachmentType.urlPreview;
|
||||||
});
|
});
|
||||||
|
|
||||||
return StreamQuotedMessageWidget(
|
return StreamQuotedMessageWidget(
|
||||||
@@ -1316,8 +1316,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
message = message.copyWith(text: '/${message.command} ${message.text}');
|
message = message.copyWith(text: '/${message.command} ${message.text}');
|
||||||
}
|
}
|
||||||
|
|
||||||
final skipEnrichUrl = _effectiveController.ogAttachment == null;
|
|
||||||
|
|
||||||
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
|
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
|
||||||
shouldKeepFocus ??= !_commandEnabled;
|
shouldKeepFocus ??= !_commandEnabled;
|
||||||
|
|
||||||
@@ -1341,10 +1339,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
await WidgetsBinding.instance.endOfFrame;
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sendOrUpdateMessage(
|
await _sendOrUpdateMessage(message: message);
|
||||||
message: message,
|
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (shouldKeepFocus) {
|
if (shouldKeepFocus) {
|
||||||
@@ -1357,36 +1352,29 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
|
|
||||||
Future<void> _sendOrUpdateMessage({
|
Future<void> _sendOrUpdateMessage({
|
||||||
required Message message,
|
required Message message,
|
||||||
bool skipEnrichUrl = false,
|
|
||||||
}) async {
|
}) async {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Future sendingFuture;
|
Future sendingFuture;
|
||||||
if (_isEditing) {
|
if (_isEditing) {
|
||||||
sendingFuture = channel.updateMessage(
|
sendingFuture = channel.updateMessage(message);
|
||||||
message,
|
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
sendingFuture = channel.sendMessage(
|
sendingFuture = channel.sendMessage(message);
|
||||||
message,
|
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await sendingFuture;
|
final resp = await sendingFuture;
|
||||||
if (resp.message?.type == 'error') {
|
if (resp.message?.isError ?? false) {
|
||||||
_effectiveController.message = message;
|
_effectiveController.message = message;
|
||||||
}
|
}
|
||||||
_startSlowMode();
|
_startSlowMode();
|
||||||
widget.onMessageSent?.call(resp.message);
|
widget.onMessageSent?.call(resp.message);
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
if (widget.onError != null) {
|
if (widget.onError != null) {
|
||||||
widget.onError?.call(e, stk);
|
return widget.onError?.call(e, stk);
|
||||||
} else {
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
|
import 'package:stream_chat_flutter/src/attachment/file_attachment.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.dart';
|
||||||
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.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/src/utils/utils.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import '../attachment/thumbnail/video_attachment_thumbnail.dart';
|
|
||||||
|
|
||||||
/// WidgetBuilder used to build the message input attachment list.
|
/// WidgetBuilder used to build the message input attachment list.
|
||||||
///
|
///
|
||||||
/// see more:
|
/// see more:
|
||||||
@@ -93,7 +90,7 @@ class _StreamMessageInputAttachmentListState
|
|||||||
|
|
||||||
// Split the attachments into file and media attachments.
|
// Split the attachments into file and media attachments.
|
||||||
for (final attachment in widget.attachments) {
|
for (final attachment in widget.attachments) {
|
||||||
if (attachment.type == 'file') {
|
if (attachment.type == AttachmentType.file) {
|
||||||
fileAttachments.add(attachment);
|
fileAttachments.add(attachment);
|
||||||
} else {
|
} else {
|
||||||
mediaAttachments.add(attachment);
|
mediaAttachments.add(attachment);
|
||||||
@@ -123,7 +120,7 @@ class _StreamMessageInputAttachmentListState
|
|||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -203,23 +200,19 @@ class MessageInputFileAttachments extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, use the default builder.
|
// Otherwise, use the default builder.
|
||||||
return ClipRRect(
|
return StreamFileAttachment(
|
||||||
key: Key(attachment.id),
|
message: Message(), // Dummy message
|
||||||
borderRadius: BorderRadius.circular(10),
|
file: attachment,
|
||||||
child: StreamFileAttachment(
|
constraints: BoxConstraints.loose(Size(
|
||||||
message: Message(), // dummy message
|
MediaQuery.of(context).size.width * 0.65,
|
||||||
file: attachment,
|
56,
|
||||||
constraints: BoxConstraints.loose(Size(
|
)),
|
||||||
MediaQuery.of(context).size.width * 0.65,
|
trailing: Padding(
|
||||||
56,
|
padding: const EdgeInsets.all(8),
|
||||||
)),
|
child: RemoveAttachmentButton(
|
||||||
trailing: Padding(
|
onPressed: onRemovePressed != null
|
||||||
padding: const EdgeInsets.all(8),
|
? () => onRemovePressed!(attachment)
|
||||||
child: RemoveAttachmentButton(
|
: null,
|
||||||
onPressed: onRemovePressed != null
|
|
||||||
? () => onRemovePressed!(attachment)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -258,7 +251,8 @@ class MessageInputMediaAttachments extends StatelessWidget {
|
|||||||
height: 104,
|
height: 104,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
|
||||||
|
cacheExtent: 104 * 10, // Cache 10 items ahead.
|
||||||
children: attachments.map<Widget>(
|
children: attachments.map<Widget>(
|
||||||
(attachment) {
|
(attachment) {
|
||||||
// If a custom builder is provided, use it.
|
// If a custom builder is provided, use it.
|
||||||
@@ -267,27 +261,47 @@ class MessageInputMediaAttachments extends StatelessWidget {
|
|||||||
return builder(context, attachment, onRemovePressed);
|
return builder(context, attachment, onRemovePressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClipRRect(
|
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||||
|
final shape = RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorTheme.borders,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
key: Key(attachment.id),
|
key: Key(attachment.id),
|
||||||
borderRadius: BorderRadius.circular(10),
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Stack(
|
decoration: ShapeDecoration(shape: shape),
|
||||||
children: <Widget>[
|
child: AspectRatio(
|
||||||
AspectRatio(
|
aspectRatio: 1,
|
||||||
aspectRatio: 1,
|
child: Stack(
|
||||||
child: MessageInputMediaAttachmentThumbnail(
|
alignment: Alignment.center,
|
||||||
attachment: attachment,
|
children: <Widget>[
|
||||||
|
StreamMediaAttachmentThumbnail(
|
||||||
|
media: attachment,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
if (attachment.type == AttachmentType.video)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 8,
|
left: 8,
|
||||||
right: 8,
|
bottom: 8,
|
||||||
child: RemoveAttachmentButton(
|
child: StreamSvgIcon.videoCall(),
|
||||||
onPressed: onRemovePressed != null
|
),
|
||||||
? () => onRemovePressed!(attachment)
|
Positioned(
|
||||||
: null,
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed!(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -297,61 +311,6 @@ class MessageInputMediaAttachments extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget that displays a thumbnail for a media attachment.
|
|
||||||
class MessageInputMediaAttachmentThumbnail extends StatelessWidget {
|
|
||||||
/// Creates a new media attachment widget.
|
|
||||||
const MessageInputMediaAttachmentThumbnail({
|
|
||||||
super.key,
|
|
||||||
required this.attachment,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The attachment to display.
|
|
||||||
final Attachment attachment;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
switch (attachment.type) {
|
|
||||||
case 'image':
|
|
||||||
case 'giphy':
|
|
||||||
return attachment.file != null
|
|
||||||
? Image.memory(
|
|
||||||
attachment.file!.bytes!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, _, __) => Image.asset(
|
|
||||||
'images/placeholder.png',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: CachedNetworkImage(
|
|
||||||
imageUrl: attachment.imageUrl ??
|
|
||||||
attachment.assetUrl ??
|
|
||||||
attachment.thumbUrl!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorWidget: (_, obj, trace) => Image.asset(
|
|
||||||
'images/placeholder.png',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case 'video':
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
StreamVideoAttachmentThumbnail(video: attachment),
|
|
||||||
Positioned(
|
|
||||||
left: 8,
|
|
||||||
bottom: 10,
|
|
||||||
child: StreamSvgIcon.videoCall(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return const ColoredBox(
|
|
||||||
color: Colors.black26,
|
|
||||||
child: Icon(Icons.insert_drive_file),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Material Button used for removing attachments.
|
/// Material Button used for removing attachments.
|
||||||
class RemoveAttachmentButton extends StatelessWidget {
|
class RemoveAttachmentButton extends StatelessWidget {
|
||||||
/// Creates a new remove attachment button.
|
/// Creates a new remove attachment button.
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
this.emptyBuilder,
|
this.emptyBuilder,
|
||||||
this.systemMessageBuilder,
|
this.systemMessageBuilder,
|
||||||
|
this.ephemeralMessageBuilder,
|
||||||
this.messageListBuilder,
|
this.messageListBuilder,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.messageFilter,
|
this.messageFilter,
|
||||||
@@ -149,6 +150,9 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
/// {@macro systemMessageBuilder}
|
/// {@macro systemMessageBuilder}
|
||||||
final SystemMessageBuilder? systemMessageBuilder;
|
final SystemMessageBuilder? systemMessageBuilder;
|
||||||
|
|
||||||
|
/// {@macro ephemeralMessageBuilder}
|
||||||
|
final EphemeralMessageBuilder? ephemeralMessageBuilder;
|
||||||
|
|
||||||
/// {@macro parentMessageBuilder}
|
/// {@macro parentMessageBuilder}
|
||||||
final ParentMessageBuilder? parentMessageBuilder;
|
final ParentMessageBuilder? parentMessageBuilder;
|
||||||
|
|
||||||
@@ -915,13 +919,19 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final currentUserMember =
|
final currentUserMember =
|
||||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||||
|
|
||||||
|
final hasFileAttachment =
|
||||||
|
message.attachments.any((it) => it.type == AttachmentType.file);
|
||||||
|
|
||||||
final hasUrlAttachment =
|
final hasUrlAttachment =
|
||||||
message.attachments.any((it) => it.ogScrapeUrl != null);
|
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
|
||||||
|
|
||||||
final isEphemeral = message.isEphemeral;
|
final attachmentBorderRadius = hasUrlAttachment
|
||||||
|
? 8.0
|
||||||
|
: hasFileAttachment
|
||||||
|
? 12.0
|
||||||
|
: 14.0;
|
||||||
|
|
||||||
final borderSide =
|
final borderSide = isOnlyEmoji ? BorderSide.none : null;
|
||||||
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
|
|
||||||
|
|
||||||
final defaultMessageWidget = StreamMessageWidget(
|
final defaultMessageWidget = StreamMessageWidget(
|
||||||
showReplyMessage: false,
|
showReplyMessage: false,
|
||||||
@@ -935,13 +945,34 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
showUsername: !isMyMessage,
|
showUsername: !isMyMessage,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
|
attachmentPadding: EdgeInsets.all(
|
||||||
|
hasUrlAttachment
|
||||||
|
? 8
|
||||||
|
: hasFileAttachment
|
||||||
|
? 4
|
||||||
|
: 2,
|
||||||
|
),
|
||||||
|
attachmentShape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: _streamTheme.colorTheme.borders,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(attachmentBorderRadius),
|
||||||
|
bottomLeft: isMyMessage
|
||||||
|
? Radius.circular(attachmentBorderRadius)
|
||||||
|
: Radius.zero,
|
||||||
|
topRight: Radius.circular(attachmentBorderRadius),
|
||||||
|
bottomRight: isMyMessage
|
||||||
|
? Radius.zero
|
||||||
|
: Radius.circular(attachmentBorderRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
borderRadiusGeometry: BorderRadius.only(
|
borderRadiusGeometry: BorderRadius.only(
|
||||||
topLeft: const Radius.circular(16),
|
topLeft: const Radius.circular(16),
|
||||||
bottomLeft:
|
bottomLeft: isMyMessage ? const Radius.circular(16) : Radius.zero,
|
||||||
isMyMessage ? const Radius.circular(16) : const Radius.circular(2),
|
|
||||||
topRight: const Radius.circular(16),
|
topRight: const Radius.circular(16),
|
||||||
bottomRight:
|
bottomRight: isMyMessage ? Radius.zero : const Radius.circular(16),
|
||||||
isMyMessage ? const Radius.circular(2) : const Radius.circular(16),
|
|
||||||
),
|
),
|
||||||
textPadding: EdgeInsets.symmetric(
|
textPadding: EdgeInsets.symmetric(
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
@@ -1061,14 +1092,8 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.isEphemeral) {
|
if (message.isEphemeral) {
|
||||||
// return widget.ephemeralMessageBuilder?.call(context, message) ??
|
return widget.ephemeralMessageBuilder?.call(context, message) ??
|
||||||
return StreamEphemeralMessage(
|
StreamEphemeralMessage(message: message);
|
||||||
message: message,
|
|
||||||
// onMessageTap: (message) {
|
|
||||||
// widget.onEphemeralMessageTap?.call(message);
|
|
||||||
// FocusScope.of(context).unfocus();
|
|
||||||
// },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final userId = StreamChat.of(context).currentUser!.id;
|
final userId = StreamChat.of(context).currentUser!.id;
|
||||||
@@ -1088,14 +1113,21 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final hasFileAttachment =
|
final hasFileAttachment =
|
||||||
message.attachments.any((it) => it.type == 'file');
|
message.attachments.any((it) => it.type == AttachmentType.file);
|
||||||
|
|
||||||
|
final hasUrlAttachment =
|
||||||
|
message.attachments.any((it) => it.type == AttachmentType.urlPreview);
|
||||||
|
|
||||||
final isThreadMessage =
|
final isThreadMessage =
|
||||||
message.parentId != null && message.showInChannel == true;
|
message.parentId != null && message.showInChannel == true;
|
||||||
|
|
||||||
final hasReplies = message.replyCount! > 0;
|
final hasReplies = message.replyCount! > 0;
|
||||||
|
|
||||||
final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0;
|
final attachmentBorderRadius = hasUrlAttachment
|
||||||
|
? 8.0
|
||||||
|
: hasFileAttachment
|
||||||
|
? 12.0
|
||||||
|
: 14.0;
|
||||||
|
|
||||||
final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
|
final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
|
||||||
!hasReplies &&
|
!hasReplies &&
|
||||||
@@ -1119,13 +1151,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
||||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||||
|
|
||||||
final isEphemeral = message.isEphemeral;
|
final borderSide = isOnlyEmoji ? BorderSide.none : null;
|
||||||
|
|
||||||
final hasUrlAttachment =
|
|
||||||
message.attachments.any((it) => it.ogScrapeUrl != null);
|
|
||||||
|
|
||||||
final borderSide =
|
|
||||||
isOnlyEmoji || hasUrlAttachment || isEphemeral ? BorderSide.none : null;
|
|
||||||
|
|
||||||
final currentUser = StreamChat.of(context).currentUser;
|
final currentUser = StreamChat.of(context).currentUser;
|
||||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||||
@@ -1170,27 +1196,39 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
showFlagButton: !isMyMessage,
|
showFlagButton: !isMyMessage,
|
||||||
borderSide: borderSide,
|
borderSide: borderSide,
|
||||||
onThreadTap: _onThreadTap,
|
onThreadTap: _onThreadTap,
|
||||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
attachmentShape: RoundedRectangleBorder(
|
||||||
topLeft: Radius.circular(attachmentBorderRadius),
|
side: BorderSide(
|
||||||
bottomLeft: isMyMessage
|
color: _streamTheme.colorTheme.borders,
|
||||||
? Radius.circular(attachmentBorderRadius)
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
: Radius.circular(
|
),
|
||||||
(hasTimeDiff || !isNextUserSame) &&
|
borderRadius: BorderRadius.only(
|
||||||
!(hasReplies || isThreadMessage || hasFileAttachment)
|
topLeft: Radius.circular(attachmentBorderRadius),
|
||||||
? 0
|
bottomLeft: isMyMessage
|
||||||
: attachmentBorderRadius,
|
? Radius.circular(attachmentBorderRadius)
|
||||||
),
|
: Radius.circular(
|
||||||
topRight: Radius.circular(attachmentBorderRadius),
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
bottomRight: isMyMessage
|
!(hasReplies || isThreadMessage || hasFileAttachment)
|
||||||
? Radius.circular(
|
? 0
|
||||||
(hasTimeDiff || !isNextUserSame) &&
|
: attachmentBorderRadius,
|
||||||
!(hasReplies || isThreadMessage || hasFileAttachment)
|
),
|
||||||
? 0
|
topRight: Radius.circular(attachmentBorderRadius),
|
||||||
: attachmentBorderRadius,
|
bottomRight: isMyMessage
|
||||||
)
|
? Radius.circular(
|
||||||
: Radius.circular(attachmentBorderRadius),
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
|
!(hasReplies || isThreadMessage || hasFileAttachment)
|
||||||
|
? 0
|
||||||
|
: attachmentBorderRadius,
|
||||||
|
)
|
||||||
|
: Radius.circular(attachmentBorderRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
attachmentPadding: EdgeInsets.all(
|
||||||
|
hasUrlAttachment
|
||||||
|
? 8
|
||||||
|
: hasFileAttachment
|
||||||
|
? 4
|
||||||
|
: 2,
|
||||||
),
|
),
|
||||||
attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2),
|
|
||||||
borderRadiusGeometry: BorderRadius.only(
|
borderRadiusGeometry: BorderRadius.only(
|
||||||
topLeft: const Radius.circular(16),
|
topLeft: const Radius.circular(16),
|
||||||
bottomLeft: isMyMessage
|
bottomLeft: isMyMessage
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class MessageCard extends StatefulWidget {
|
|||||||
required this.isGiphy,
|
required this.isGiphy,
|
||||||
required this.attachmentBuilders,
|
required this.attachmentBuilders,
|
||||||
required this.attachmentPadding,
|
required this.attachmentPadding,
|
||||||
|
required this.attachmentShape,
|
||||||
required this.onAttachmentTap,
|
required this.onAttachmentTap,
|
||||||
required this.onShowMessage,
|
required this.onShowMessage,
|
||||||
required this.onReplyTap,
|
required this.onReplyTap,
|
||||||
@@ -80,6 +81,9 @@ class MessageCard extends StatefulWidget {
|
|||||||
/// {@macro attachmentPadding}
|
/// {@macro attachmentPadding}
|
||||||
final EdgeInsetsGeometry attachmentPadding;
|
final EdgeInsetsGeometry attachmentPadding;
|
||||||
|
|
||||||
|
/// {@macro attachmentShape}
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
/// {@macro onAttachmentTap}
|
/// {@macro onAttachmentTap}
|
||||||
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
||||||
|
|
||||||
@@ -200,6 +204,7 @@ class _MessageCardState extends State<MessageCard> {
|
|||||||
message: widget.message,
|
message: widget.message,
|
||||||
attachmentBuilders: widget.attachmentBuilders,
|
attachmentBuilders: widget.attachmentBuilders,
|
||||||
attachmentPadding: widget.attachmentPadding,
|
attachmentPadding: widget.attachmentPadding,
|
||||||
|
attachmentShape: widget.attachmentShape,
|
||||||
onAttachmentTap: widget.onAttachmentTap,
|
onAttachmentTap: widget.onAttachmentTap,
|
||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
onReplyTap: widget.onReplyTap,
|
onReplyTap: widget.onReplyTap,
|
||||||
|
|||||||
@@ -49,11 +49,9 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.translateUserAvatar = true,
|
this.translateUserAvatar = true,
|
||||||
this.shape,
|
this.shape,
|
||||||
this.attachmentShape,
|
|
||||||
this.borderSide,
|
this.borderSide,
|
||||||
this.attachmentBorderSide,
|
|
||||||
this.borderRadiusGeometry,
|
this.borderRadiusGeometry,
|
||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentShape,
|
||||||
this.onMentionTap,
|
this.onMentionTap,
|
||||||
this.onMessageTap,
|
this.onMessageTap,
|
||||||
this.showReactionPicker = true,
|
this.showReactionPicker = true,
|
||||||
@@ -334,21 +332,11 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final BorderSide? borderSide;
|
final BorderSide? borderSide;
|
||||||
|
|
||||||
/// {@template attachmentBorderSide}
|
|
||||||
/// The borderSide of an attachment
|
|
||||||
/// {@endtemplate}
|
|
||||||
final BorderSide? attachmentBorderSide;
|
|
||||||
|
|
||||||
/// {@template borderRadiusGeometry}
|
/// {@template borderRadiusGeometry}
|
||||||
/// The border radius of the message text
|
/// The border radius of the message text
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||||
|
|
||||||
/// {@template attachmentBorderRadiusGeometry}
|
|
||||||
/// The border radius of an attachment
|
|
||||||
/// {@endtemplate}
|
|
||||||
final BorderRadiusGeometry? attachmentBorderRadiusGeometry;
|
|
||||||
|
|
||||||
/// {@template padding}
|
/// {@template padding}
|
||||||
/// The padding of the widget
|
/// The padding of the widget
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@@ -542,9 +530,7 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
ShapeBorder? shape,
|
ShapeBorder? shape,
|
||||||
ShapeBorder? attachmentShape,
|
ShapeBorder? attachmentShape,
|
||||||
BorderSide? borderSide,
|
BorderSide? borderSide,
|
||||||
BorderSide? attachmentBorderSide,
|
|
||||||
BorderRadiusGeometry? borderRadiusGeometry,
|
BorderRadiusGeometry? borderRadiusGeometry,
|
||||||
BorderRadiusGeometry? attachmentBorderRadiusGeometry,
|
|
||||||
EdgeInsetsGeometry? padding,
|
EdgeInsetsGeometry? padding,
|
||||||
EdgeInsets? textPadding,
|
EdgeInsets? textPadding,
|
||||||
EdgeInsetsGeometry? attachmentPadding,
|
EdgeInsetsGeometry? attachmentPadding,
|
||||||
@@ -603,10 +589,7 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
shape: shape ?? this.shape,
|
shape: shape ?? this.shape,
|
||||||
attachmentShape: attachmentShape ?? this.attachmentShape,
|
attachmentShape: attachmentShape ?? this.attachmentShape,
|
||||||
borderSide: borderSide ?? this.borderSide,
|
borderSide: borderSide ?? this.borderSide,
|
||||||
attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide,
|
|
||||||
borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry,
|
borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry,
|
||||||
attachmentBorderRadiusGeometry:
|
|
||||||
attachmentBorderRadiusGeometry ?? this.attachmentBorderRadiusGeometry,
|
|
||||||
padding: padding ?? this.padding,
|
padding: padding ?? this.padding,
|
||||||
textPadding: textPadding ?? this.textPadding,
|
textPadding: textPadding ?? this.textPadding,
|
||||||
attachmentPadding: attachmentPadding ?? this.attachmentPadding,
|
attachmentPadding: attachmentPadding ?? this.attachmentPadding,
|
||||||
@@ -691,8 +674,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
/// {@template isGiphy}
|
/// {@template isGiphy}
|
||||||
/// `true` if any of the [message]'s attachments are a giphy.
|
/// `true` if any of the [message]'s attachments are a giphy.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
bool get isGiphy =>
|
bool get isGiphy => widget.message.attachments
|
||||||
widget.message.attachments.any((element) => element.type == 'giphy');
|
.any((element) => element.type == AttachmentType.giphy);
|
||||||
|
|
||||||
/// {@template isOnlyEmoji}
|
/// {@template isOnlyEmoji}
|
||||||
/// `true` if [message.text] contains only emoji.
|
/// `true` if [message.text] contains only emoji.
|
||||||
@@ -749,7 +732,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
bool get shouldShowEditAction =>
|
bool get shouldShowEditAction =>
|
||||||
widget.showEditMessage &&
|
widget.showEditMessage &&
|
||||||
!isDeleteFailed &&
|
!isDeleteFailed &&
|
||||||
!widget.message.attachments.any((element) => element.type == 'giphy');
|
!widget.message.attachments
|
||||||
|
.any((element) => element.type == AttachmentType.giphy);
|
||||||
|
|
||||||
bool get shouldShowResendAction =>
|
bool get shouldShowResendAction =>
|
||||||
widget.showResendMessage && (isSendFailed || isUpdateFailed);
|
widget.showResendMessage && (isSendFailed || isUpdateFailed);
|
||||||
@@ -762,7 +746,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
bool get shouldShowEditMessage =>
|
bool get shouldShowEditMessage =>
|
||||||
widget.showEditMessage &&
|
widget.showEditMessage &&
|
||||||
!isDeleteFailed &&
|
!isDeleteFailed &&
|
||||||
!widget.message.attachments.any((element) => element.type == 'giphy');
|
!widget.message.attachments
|
||||||
|
.any((element) => element.type == AttachmentType.giphy);
|
||||||
|
|
||||||
bool get shouldShowThreadReplyAction =>
|
bool get shouldShowThreadReplyAction =>
|
||||||
widget.showThreadReplyMessage &&
|
widget.showThreadReplyMessage &&
|
||||||
@@ -853,6 +838,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
textPadding: widget.textPadding,
|
textPadding: widget.textPadding,
|
||||||
attachmentBuilders: widget.attachmentBuilders,
|
attachmentBuilders: widget.attachmentBuilders,
|
||||||
attachmentPadding: widget.attachmentPadding,
|
attachmentPadding: widget.attachmentPadding,
|
||||||
|
attachmentShape: widget.attachmentShape,
|
||||||
onAttachmentTap: widget.onAttachmentTap,
|
onAttachmentTap: widget.onAttachmentTap,
|
||||||
onReplyTap: widget.onReplyTap,
|
onReplyTap: widget.onReplyTap,
|
||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
required this.isGiphy,
|
required this.isGiphy,
|
||||||
required this.attachmentBuilders,
|
required this.attachmentBuilders,
|
||||||
required this.attachmentPadding,
|
required this.attachmentPadding,
|
||||||
|
required this.attachmentShape,
|
||||||
required this.onAttachmentTap,
|
required this.onAttachmentTap,
|
||||||
required this.onShowMessage,
|
required this.onShowMessage,
|
||||||
required this.onReplyTap,
|
required this.onReplyTap,
|
||||||
@@ -150,6 +151,9 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
/// {@macro attachmentPadding}
|
/// {@macro attachmentPadding}
|
||||||
final EdgeInsetsGeometry attachmentPadding;
|
final EdgeInsetsGeometry attachmentPadding;
|
||||||
|
|
||||||
|
/// {@macro attachmentShape}
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
/// {@macro onAttachmentTap}
|
/// {@macro onAttachmentTap}
|
||||||
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
||||||
|
|
||||||
@@ -341,6 +345,7 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
isGiphy: isGiphy,
|
isGiphy: isGiphy,
|
||||||
attachmentBuilders: attachmentBuilders,
|
attachmentBuilders: attachmentBuilders,
|
||||||
attachmentPadding: attachmentPadding,
|
attachmentPadding: attachmentPadding,
|
||||||
|
attachmentShape: attachmentShape,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
onReplyTap: onReplyTap,
|
onReplyTap: onReplyTap,
|
||||||
onShowMessage: onShowMessage,
|
onShowMessage: onShowMessage,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ParseAttachments extends StatelessWidget {
|
|||||||
required this.message,
|
required this.message,
|
||||||
required this.attachmentBuilders,
|
required this.attachmentBuilders,
|
||||||
required this.attachmentPadding,
|
required this.attachmentPadding,
|
||||||
|
this.attachmentShape,
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.onReplyTap,
|
this.onReplyTap,
|
||||||
@@ -31,6 +32,9 @@ class ParseAttachments extends StatelessWidget {
|
|||||||
/// {@macro attachmentPadding}
|
/// {@macro attachmentPadding}
|
||||||
final EdgeInsetsGeometry attachmentPadding;
|
final EdgeInsetsGeometry attachmentPadding;
|
||||||
|
|
||||||
|
/// {@macro attachmentShape}
|
||||||
|
final ShapeBorder? attachmentShape;
|
||||||
|
|
||||||
/// {@macro onAttachmentTap}
|
/// {@macro onAttachmentTap}
|
||||||
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
final StreamAttachmentWidgetTapCallback? onAttachmentTap;
|
||||||
|
|
||||||
@@ -104,6 +108,8 @@ class ParseAttachments extends StatelessWidget {
|
|||||||
var builders = attachmentBuilders;
|
var builders = attachmentBuilders;
|
||||||
builders ??= StreamAttachmentWidgetBuilder.defaultBuilders(
|
builders ??= StreamAttachmentWidgetBuilder.defaultBuilders(
|
||||||
message: message,
|
message: message,
|
||||||
|
shape: attachmentShape,
|
||||||
|
padding: attachmentPadding,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -50,45 +50,47 @@ class StreamMessageReactionsModal extends StatelessWidget {
|
|||||||
|
|
||||||
final child = Center(
|
final child = Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: SafeArea(
|
||||||
padding: const EdgeInsets.all(8),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(8),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
if (showReactionPicker && hasReactionPermission)
|
children: <Widget>[
|
||||||
LayoutBuilder(
|
if (showReactionPicker && hasReactionPermission)
|
||||||
builder: (context, constraints) {
|
LayoutBuilder(
|
||||||
return Align(
|
builder: (context, constraints) {
|
||||||
alignment: Alignment(
|
return Align(
|
||||||
calculateReactionsHorizontalAlignment(
|
alignment: Alignment(
|
||||||
user,
|
calculateReactionsHorizontalAlignment(
|
||||||
message,
|
user,
|
||||||
constraints,
|
message,
|
||||||
fontSize,
|
constraints,
|
||||||
orientation,
|
fontSize,
|
||||||
|
orientation,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0,
|
child: StreamReactionPicker(
|
||||||
),
|
message: message,
|
||||||
child: StreamReactionPicker(
|
),
|
||||||
message: message,
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
const SizedBox(height: 10),
|
||||||
),
|
IgnorePointer(
|
||||||
const SizedBox(height: 10),
|
child: messageWidget,
|
||||||
IgnorePointer(
|
|
||||||
child: messageWidget,
|
|
||||||
),
|
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
ReactionsCard(
|
|
||||||
currentUser: user!,
|
|
||||||
message: message,
|
|
||||||
messageTheme: messageTheme,
|
|
||||||
),
|
),
|
||||||
|
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ReactionsCard(
|
||||||
|
currentUser: user!,
|
||||||
|
message: message,
|
||||||
|
messageTheme: messageTheme,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class TextBubble extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (message.text?.trim().isEmpty ?? false) return const Offstage();
|
if (message.text?.trim().isEmpty ?? true) return const Offstage();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: isOnlyEmoji ? EdgeInsets.zero : textPadding,
|
padding: isOnlyEmoji ? EdgeInsets.zero : textPadding,
|
||||||
child: textBuilder != null
|
child: textBuilder != null
|
||||||
|
|||||||
@@ -228,8 +228,7 @@ class StreamChannelListTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final hasNonUrlAttachments = lastMessage.attachments
|
final hasNonUrlAttachments = lastMessage.attachments
|
||||||
.where((it) => it.titleLink == null || it.type == 'giphy')
|
.any((it) => it.type != AttachmentType.urlPreview);
|
||||||
.isNotEmpty;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ class StreamChatThemeData {
|
|||||||
urlAttachmentTitleStyle: textTheme.footnoteBold,
|
urlAttachmentTitleStyle: textTheme.footnoteBold,
|
||||||
urlAttachmentTextStyle: textTheme.footnote,
|
urlAttachmentTextStyle: textTheme.footnote,
|
||||||
urlAttachmentTitleMaxLine: 1,
|
urlAttachmentTitleMaxLine: 1,
|
||||||
|
urlAttachmentTextMaxLine: 3,
|
||||||
),
|
),
|
||||||
otherMessageTheme: StreamMessageThemeData(
|
otherMessageTheme: StreamMessageThemeData(
|
||||||
reactionsBackgroundColor: colorTheme.borders,
|
reactionsBackgroundColor: colorTheme.borders,
|
||||||
@@ -227,6 +228,7 @@ class StreamChatThemeData {
|
|||||||
urlAttachmentTitleStyle: textTheme.footnoteBold,
|
urlAttachmentTitleStyle: textTheme.footnoteBold,
|
||||||
urlAttachmentTextStyle: textTheme.footnote,
|
urlAttachmentTextStyle: textTheme.footnote,
|
||||||
urlAttachmentTitleMaxLine: 1,
|
urlAttachmentTitleMaxLine: 1,
|
||||||
|
urlAttachmentTextMaxLine: 3,
|
||||||
),
|
),
|
||||||
messageInputTheme: StreamMessageInputThemeData(
|
messageInputTheme: StreamMessageInputThemeData(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:diacritic/diacritic.dart';
|
import 'package:diacritic/diacritic.dart';
|
||||||
@@ -5,6 +6,8 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.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:stream_chat_flutter/src/localization/translations.dart';
|
import 'package:stream_chat_flutter/src/localization/translations.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
@@ -114,7 +117,7 @@ extension PlatformFileX on PlatformFile {
|
|||||||
final file = toAttachmentFile;
|
final file = toAttachmentFile;
|
||||||
final extraDataMap = <String, Object>{};
|
final extraDataMap = <String, Object>{};
|
||||||
|
|
||||||
final mimeType = file.mimeType?.mimeType;
|
final mimeType = file.mediaType?.mimeType;
|
||||||
|
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
extraDataMap['mime_type'] = mimeType;
|
extraDataMap['mime_type'] = mimeType;
|
||||||
@@ -151,7 +154,7 @@ extension XFileX on XFile {
|
|||||||
|
|
||||||
final extraDataMap = <String, Object>{};
|
final extraDataMap = <String, Object>{};
|
||||||
|
|
||||||
final mimeType = this.mimeType ?? file.mimeType?.mimeType;
|
final mimeType = this.mimeType ?? file.mediaType?.mimeType;
|
||||||
|
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
extraDataMap['mime_type'] = mimeType;
|
extraDataMap['mime_type'] = mimeType;
|
||||||
@@ -367,7 +370,7 @@ extension MessageX on Message {
|
|||||||
|
|
||||||
/// Returns an approximation of message size
|
/// Returns an approximation of message size
|
||||||
double roughMessageSize(double? fontSize) {
|
double roughMessageSize(double? fontSize) {
|
||||||
var messageTextLength = min(text!.biggestLine().length, 65);
|
var messageTextLength = min(text?.biggestLine().length ?? 0, 65);
|
||||||
|
|
||||||
if (quotedMessage != null) {
|
if (quotedMessage != null) {
|
||||||
var quotedMessageLength =
|
var quotedMessageLength =
|
||||||
@@ -488,3 +491,46 @@ extension ConstraintsX on BoxConstraints {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Useful extensions on [Attachment].
|
||||||
|
extension OriginalSizeX 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 != AttachmentType.image && type != AttachmentType.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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final size = ImageSizeGetter.getSize(input);
|
||||||
|
if (size.needRotate) {
|
||||||
|
return Size(size.height.toDouble(), size.width.toDouble());
|
||||||
|
}
|
||||||
|
return Size(size.width.toDouble(), size.height.toDouble());
|
||||||
|
} catch (e, stk) {
|
||||||
|
debugPrint('Error getting image size: $e\n$stk');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -259,6 +259,14 @@ typedef SystemMessageBuilder = Widget Function(
|
|||||||
Message,
|
Message,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// {@template ephemeralMessageBuilder}
|
||||||
|
/// A widget builder for creating custom ephemeral messages.
|
||||||
|
/// {@endtemplate}
|
||||||
|
typedef EphemeralMessageBuilder = Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
Message,
|
||||||
|
);
|
||||||
|
|
||||||
/// {@template threadBuilder}
|
/// {@template threadBuilder}
|
||||||
/// A widget builder for creating custom thread UI.
|
/// A widget builder for creating custom thread UI.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ class StreamVideoThumbnailImage
|
|||||||
int get hashCode => Object.hash(video, scale);
|
int get hashCode => Object.hash(video, scale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() {
|
||||||
'${objectRuntimeType(this, 'StreamVideoThumbnailImage')}($video, scale: $scale)';
|
final runtimeType = objectRuntimeType(this, 'StreamVideoThumbnailImage');
|
||||||
|
return '$runtimeType($video, scale: $scale)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
export 'src/attachment/attachment_title.dart';
|
export 'src/attachment/attachment_title.dart';
|
||||||
|
export 'src/attachment/gallery_attachment.dart';
|
||||||
export 'src/attachment/handler/stream_attachment_handler.dart';
|
export 'src/attachment/handler/stream_attachment_handler.dart';
|
||||||
export 'src/attachment/image_attachment.dart';
|
export 'src/attachment/image_attachment.dart';
|
||||||
export 'src/attachment/gallery_attachment.dart';
|
|
||||||
export 'src/attachment/stream_attachment_package.dart';
|
export 'src/attachment/stream_attachment_package.dart';
|
||||||
export 'src/attachment/url_attachment.dart';
|
export 'src/attachment/url_attachment.dart';
|
||||||
export 'src/attachment/video_attachment.dart';
|
export 'src/attachment/video_attachment.dart';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import '../mocks.dart';
|
import '../mocks.dart';
|
||||||
@@ -18,6 +19,27 @@ void main() {
|
|||||||
final themeData = ThemeData();
|
final themeData = ThemeData();
|
||||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||||
|
|
||||||
|
final attachments = [
|
||||||
|
Attachment(
|
||||||
|
type: 'image',
|
||||||
|
title: 'example.png',
|
||||||
|
imageUrl:
|
||||||
|
'https://logowik.com/content/uploads/images/flutter5786.jpg',
|
||||||
|
extraData: const {
|
||||||
|
'mime_type': 'png',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Attachment(
|
||||||
|
type: 'image',
|
||||||
|
title: 'example.png',
|
||||||
|
imageUrl:
|
||||||
|
'https://logowik.com/content/uploads/images/flutter5786.jpg',
|
||||||
|
extraData: const {
|
||||||
|
'mime_type': 'png',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: StreamChatTheme(
|
home: StreamChatTheme(
|
||||||
@@ -31,26 +53,17 @@ void main() {
|
|||||||
300,
|
300,
|
||||||
)),
|
)),
|
||||||
message: Message(),
|
message: Message(),
|
||||||
attachments: [
|
attachments: attachments,
|
||||||
Attachment(
|
itemBuilder: (context, index) {
|
||||||
type: 'image',
|
final attachment = attachments[index];
|
||||||
title: 'example.png',
|
|
||||||
imageUrl:
|
return StreamImageAttachmentThumbnail(
|
||||||
'https://logowik.com/content/uploads/images/flutter5786.jpg',
|
image: attachment,
|
||||||
extraData: const {
|
width: double.infinity,
|
||||||
'mime_type': 'png',
|
height: double.infinity,
|
||||||
},
|
fit: BoxFit.cover,
|
||||||
),
|
);
|
||||||
Attachment(
|
},
|
||||||
type: 'image',
|
|
||||||
title: 'example.png',
|
|
||||||
imageUrl:
|
|
||||||
'https://logowik.com/content/uploads/images/flutter5786.jpg',
|
|
||||||
extraData: const {
|
|
||||||
'mime_type': 'png',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -30,7 +30,7 @@ void main() {
|
|||||||
300,
|
300,
|
||||||
)),
|
)),
|
||||||
message: Message(),
|
message: Message(),
|
||||||
file: Attachment(
|
giphy: Attachment(
|
||||||
type: 'giphy',
|
type: 'giphy',
|
||||||
title: 'example.gif',
|
title: 'example.gif',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ void main() {
|
|||||||
300,
|
300,
|
||||||
)),
|
)),
|
||||||
message: Message(),
|
message: Message(),
|
||||||
file: Attachment(
|
image: Attachment(
|
||||||
type: 'image',
|
type: 'image',
|
||||||
title: 'example.png',
|
title: 'example.png',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ void main() {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
child: StreamUrlAttachment(
|
child: StreamUrlAttachment(
|
||||||
messageTheme: streamTheme.ownMessageTheme,
|
messageTheme: streamTheme.ownMessageTheme,
|
||||||
|
message: Message(),
|
||||||
hostDisplayName: 'Test',
|
hostDisplayName: 'Test',
|
||||||
urlAttachment: Attachment(
|
urlAttachment: Attachment(
|
||||||
title: 'Flutter',
|
title: 'Flutter',
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -70,7 +70,7 @@ final _channelThemeControlMidLerp = StreamChannelHeaderThemeData(
|
|||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
color: const Color(0xff101418),
|
color: const Color(0xff111417),
|
||||||
titleStyle: const TextStyle(
|
titleStyle: const TextStyle(
|
||||||
color: Color(0xffffffff),
|
color: Color(0xffffffff),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData(
|
|||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
color: const Color(0xff87898b),
|
color: const Color(0xff88898a),
|
||||||
titleStyle: const TextStyle(
|
titleStyle: const TextStyle(
|
||||||
color: Color(0xff7f7f7f),
|
color: Color(0xff7f7f7f),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -73,11 +73,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return const SizedBox.shrink();
|
||||||
appBar: StreamGalleryFooter(
|
|
||||||
mediaAttachmentPackages: Message().getAttachmentPackageList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -116,11 +112,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return const SizedBox.shrink();
|
||||||
appBar: StreamGalleryFooter(
|
|
||||||
mediaAttachmentPackages: Message().getAttachmentPackageList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -160,7 +152,7 @@ final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData(
|
|||||||
|
|
||||||
// Mid-lerp theme control
|
// Mid-lerp theme control
|
||||||
const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
|
const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
|
||||||
backgroundColor: Color(0xff87898b),
|
backgroundColor: Color(0xff88898a),
|
||||||
shareIconColor: Color(0xff7f7f7f),
|
shareIconColor: Color(0xff7f7f7f),
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
color: Color(0xff7f7f7f),
|
color: Color(0xff7f7f7f),
|
||||||
@@ -169,7 +161,7 @@ const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData(
|
|||||||
),
|
),
|
||||||
gridIconButtonColor: Color(0xff7f7f7f),
|
gridIconButtonColor: Color(0xff7f7f7f),
|
||||||
bottomSheetBarrierColor: Color(0x4c000000),
|
bottomSheetBarrierColor: Color(0x4c000000),
|
||||||
bottomSheetBackgroundColor: Color(0xff87898b),
|
bottomSheetBackgroundColor: Color(0xff88898a),
|
||||||
bottomSheetPhotosTextStyle: TextStyle(
|
bottomSheetPhotosTextStyle: TextStyle(
|
||||||
color: Color(0xff7f7f7f),
|
color: Color(0xff7f7f7f),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -66,22 +66,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
final attachment = Attachment(
|
return const SizedBox.shrink();
|
||||||
type: 'video',
|
|
||||||
title: 'video.mp4',
|
|
||||||
);
|
|
||||||
final _message = Message(
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
attachments: [
|
|
||||||
attachment,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return Scaffold(
|
|
||||||
appBar: StreamGalleryHeader(
|
|
||||||
message: _message,
|
|
||||||
attachment: _message.attachments[0],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -116,22 +101,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
final attachment = Attachment(
|
return const SizedBox.shrink();
|
||||||
type: 'video',
|
|
||||||
title: 'video.mp4',
|
|
||||||
);
|
|
||||||
final _message = Message(
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
attachments: [
|
|
||||||
attachment,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return Scaffold(
|
|
||||||
appBar: StreamGalleryHeader(
|
|
||||||
message: _message,
|
|
||||||
attachment: _message.attachments[0],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -175,7 +145,7 @@ final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData(
|
|||||||
// Light theme test control.
|
// Light theme test control.
|
||||||
final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
|
final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
|
||||||
closeButtonColor: const Color(0xff7f7f7f),
|
closeButtonColor: const Color(0xff7f7f7f),
|
||||||
backgroundColor: const Color(0xff87898b),
|
backgroundColor: const Color(0xff88898a),
|
||||||
iconMenuPointColor: const Color(0xff7f7f7f),
|
iconMenuPointColor: const Color(0xff7f7f7f),
|
||||||
titleTextStyle: const TextStyle(
|
titleTextStyle: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -194,7 +164,7 @@ final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData(
|
|||||||
// Dark theme test control.
|
// Dark theme test control.
|
||||||
final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData(
|
final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData(
|
||||||
closeButtonColor: const Color(0xffffffff),
|
closeButtonColor: const Color(0xffffffff),
|
||||||
backgroundColor: const Color(0xff101418),
|
backgroundColor: const Color(0xff121416),
|
||||||
iconMenuPointColor: const Color(0xffffffff),
|
iconMenuPointColor: const Color(0xffffffff),
|
||||||
titleTextStyle: const TextStyle(
|
titleTextStyle: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final _messageInputThemeControl = StreamMessageInputThemeData(
|
|||||||
final _messageInputThemeControlMidLerp = StreamMessageInputThemeData(
|
final _messageInputThemeControlMidLerp = StreamMessageInputThemeData(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
sendAnimationDuration: const Duration(milliseconds: 300),
|
sendAnimationDuration: const Duration(milliseconds: 300),
|
||||||
inputBackgroundColor: const Color(0xff87898b),
|
inputBackgroundColor: const Color(0xff88898a),
|
||||||
actionButtonColor: const Color(0xff196eff),
|
actionButtonColor: const Color(0xff196eff),
|
||||||
actionButtonIdleColor: const Color(0xff7a7a7a),
|
actionButtonIdleColor: const Color(0xff7a7a7a),
|
||||||
sendButtonColor: const Color(0xff196eff),
|
sendButtonColor: const Color(0xff196eff),
|
||||||
|
|||||||
@@ -68,12 +68,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return const SizedBox.shrink();
|
||||||
body: StreamChannel(
|
|
||||||
channel: MockChannel(),
|
|
||||||
child: const StreamMessageListView(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,12 +93,7 @@ void main() {
|
|||||||
home: Builder(
|
home: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return const SizedBox.shrink();
|
||||||
body: StreamChannel(
|
|
||||||
channel: MockChannel(),
|
|
||||||
child: const StreamMessageListView(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -151,7 +141,7 @@ final _messageListViewThemeDataControl = StreamMessageListViewThemeData(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData(
|
const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData(
|
||||||
backgroundColor: Color(0xff87898b),
|
backgroundColor: Color(0xff88898a),
|
||||||
);
|
);
|
||||||
|
|
||||||
final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData(
|
final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData(
|
||||||
|
|||||||