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