Merge branch 'develop' into refactor/message-status
This commit is contained in:
@@ -7,6 +7,26 @@
|
|||||||
- [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error
|
- [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error
|
||||||
in `SendingIndicatorBuilder`.
|
in `SendingIndicatorBuilder`.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added support for customizing attachments in `StreamMessageInput`. Use various properties mentioned
|
||||||
|
below. [#1511](https://github.com/GetStream/stream-chat-flutter/issues/1511)
|
||||||
|
* `StreamMessageInput.attachmentListBuilder` to customize the attachment list.
|
||||||
|
* `StreamMessageInput.fileAttachmentListBuilder` to customize the file attachment list.
|
||||||
|
* `StreamMessageInput.mediaAttachmentListBuilder` to customize the media attachment list. Includes images, videos
|
||||||
|
and gifs.
|
||||||
|
* `StreamMessageInput.fileAttachmentBuilder` to customize the file attachment item shown in `FileAttachmentList`.
|
||||||
|
* `StreamMessageInput.mediaAttachmentBuilder` to customize the media attachment item shown in
|
||||||
|
`MediaAttachmentList`.
|
||||||
|
|
||||||
|
|
||||||
|
- Added `StreamMessageInput.quotedMessageAttachmentThumbnailBuilders` to customize the thumbnail builders for quoted
|
||||||
|
message attachments.
|
||||||
|
|
||||||
|
🔄 Changed
|
||||||
|
|
||||||
|
- Deprecated `StreamMessageInput.attachmentThumbnailBuilders` in favor of `StreamMessageInput.mediaAttachmentBuilder`.
|
||||||
|
|
||||||
## 6.4.0
|
## 6.4.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@template streamVideoAttachment}
|
/// {@template streamVideoAttachment}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@template streamGalleryFooter}
|
/// {@template streamGalleryFooter}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart'
|
|
||||||
hide ErrorListener;
|
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart';
|
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/attachment_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/attachment_button.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/command_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/command_button.dart';
|
||||||
@@ -19,7 +15,6 @@ import 'package:stream_chat_flutter/src/message_input/quoted_message_widget.dart
|
|||||||
import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart';
|
import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart';
|
import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/tld.dart';
|
import 'package:stream_chat_flutter/src/message_input/tld.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
const _kCommandTrigger = '/';
|
const _kCommandTrigger = '/';
|
||||||
@@ -110,6 +105,12 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.messageInputController,
|
this.messageInputController,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
|
this.attachmentListBuilder,
|
||||||
|
this.fileAttachmentListBuilder,
|
||||||
|
this.mediaAttachmentListBuilder,
|
||||||
|
this.fileAttachmentBuilder,
|
||||||
|
this.mediaAttachmentBuilder,
|
||||||
|
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.sendButtonLocation = SendButtonLocation.outside,
|
this.sendButtonLocation = SendButtonLocation.outside,
|
||||||
@@ -130,6 +131,7 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
this.sendButtonBuilder,
|
this.sendButtonBuilder,
|
||||||
this.quotedMessageBuilder,
|
this.quotedMessageBuilder,
|
||||||
|
this.quotedMessageAttachmentThumbnailBuilders,
|
||||||
this.shouldKeepFocusAfterMessage,
|
this.shouldKeepFocusAfterMessage,
|
||||||
this.validator = _defaultValidator,
|
this.validator = _defaultValidator,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
@@ -209,9 +211,41 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// The location of the custom actions.
|
/// The location of the custom actions.
|
||||||
final ActionsLocation actionsLocation;
|
final ActionsLocation actionsLocation;
|
||||||
|
|
||||||
|
/// Builder used to build the attachment list present in the message input.
|
||||||
|
///
|
||||||
|
/// In case you want to customize only sub-parts of the attachment list,
|
||||||
|
/// consider using [fileAttachmentListBuilder], [mediaAttachmentListBuilder].
|
||||||
|
final AttachmentListBuilder? attachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file type attachment list.
|
||||||
|
///
|
||||||
|
/// In case you want to customize the attachment item, consider using
|
||||||
|
/// [fileAttachmentBuilder].
|
||||||
|
final AttachmentListBuilder? fileAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media type attachment list.
|
||||||
|
///
|
||||||
|
/// In case you want to customize the attachment item, consider using
|
||||||
|
/// [mediaAttachmentBuilder].
|
||||||
|
final AttachmentListBuilder? mediaAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment item.
|
||||||
|
final AttachmentItemBuilder? fileAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment item.
|
||||||
|
final AttachmentItemBuilder? mediaAttachmentBuilder;
|
||||||
|
|
||||||
/// Map that defines a thumbnail builder for an attachment type.
|
/// Map that defines a thumbnail builder for an attachment type.
|
||||||
|
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||||
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
||||||
|
|
||||||
|
/// Map that defines a thumbnail builder for an attachment type.
|
||||||
|
///
|
||||||
|
/// This is used to build the thumbnail for the attachment in the quoted
|
||||||
|
/// message.
|
||||||
|
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||||
|
quotedMessageAttachmentThumbnailBuilders;
|
||||||
|
|
||||||
/// The focus node associated to the TextField.
|
/// The focus node associated to the TextField.
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
@@ -1146,193 +1180,93 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
messageTheme: _streamChatTheme.otherMessageTheme,
|
messageTheme: _streamChatTheme.otherMessageTheme,
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
||||||
attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders,
|
attachmentThumbnailBuilders:
|
||||||
|
widget.quotedMessageAttachmentThumbnailBuilders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
final nonOGAttachments = _effectiveController.attachments.where(
|
final attachments = _effectiveController.attachments;
|
||||||
(it) => it.titleLink == null,
|
final nonOGAttachments = attachments.where((it) {
|
||||||
);
|
return it.titleLink == null;
|
||||||
|
}).toList(growable: false);
|
||||||
|
|
||||||
|
// If there are no attachments, return an empty widget
|
||||||
if (nonOGAttachments.isEmpty) return const Offstage();
|
if (nonOGAttachments.isEmpty) return const Offstage();
|
||||||
final fileAttachments = nonOGAttachments
|
|
||||||
.where((it) => it.type == 'file')
|
|
||||||
.toList(growable: false);
|
|
||||||
final remainingAttachments = nonOGAttachments
|
|
||||||
.where((it) => it.type != 'file')
|
|
||||||
.toList(growable: false);
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
if (fileAttachments.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
|
||||||
child: LimitedBox(
|
|
||||||
maxHeight: 136,
|
|
||||||
child: ListView(
|
|
||||||
reverse: true,
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: fileAttachments.reversed
|
|
||||||
.map<Widget>(
|
|
||||||
(e) => ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: StreamFileAttachment(
|
|
||||||
message: Message(), // dummy message
|
|
||||||
attachment: e,
|
|
||||||
constraints: BoxConstraints.loose(Size(
|
|
||||||
MediaQuery.of(context).size.width * 0.65,
|
|
||||||
56,
|
|
||||||
)),
|
|
||||||
trailing: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: _buildRemoveButton(e),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.insertBetween(const SizedBox(height: 8)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (remainingAttachments.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
|
||||||
child: LimitedBox(
|
|
||||||
maxHeight: 104,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: remainingAttachments
|
|
||||||
.map<Widget>(
|
|
||||||
(attachment) => ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 104,
|
|
||||||
width: 104,
|
|
||||||
child: _buildAttachment(attachment),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: _buildRemoveButton(attachment),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.insertBetween(const SizedBox(width: 8)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRemoveButton(Attachment attachment) {
|
// Default callback for removing an attachment.
|
||||||
return SizedBox(
|
Future<void> onAttachmentRemovePressed(Attachment attachment) async {
|
||||||
height: 24,
|
final file = attachment.file;
|
||||||
width: 24,
|
final uploadState = attachment.uploadState;
|
||||||
child: RawMaterialButton(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
highlightElevation: 0,
|
|
||||||
focusElevation: 0,
|
|
||||||
hoverElevation: 0,
|
|
||||||
onPressed: () async {
|
|
||||||
final file = attachment.file;
|
|
||||||
final uploadState = attachment.uploadState;
|
|
||||||
|
|
||||||
if (file != null && !uploadState.isSuccess && !isWeb) {
|
if (file != null && !uploadState.isSuccess && !isWeb) {
|
||||||
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
||||||
attachmentFile: file,
|
attachmentFile: file,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_effectiveController.removeAttachmentById(attachment.id);
|
_effectiveController.removeAttachmentById(attachment.id);
|
||||||
},
|
}
|
||||||
fillColor:
|
|
||||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
|
||||||
child: Center(
|
|
||||||
child: StreamSvgIcon.close(
|
|
||||||
size: 24,
|
|
||||||
color: _streamChatTheme.colorTheme.barsBg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAttachment(Attachment attachment) {
|
// If the user has provided a custom attachment list builder, use that.
|
||||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
final attachmentListBuilder = widget.attachmentListBuilder;
|
||||||
true) {
|
if (attachmentListBuilder != null) {
|
||||||
return widget.attachmentThumbnailBuilders![attachment.type!]!(
|
return attachmentListBuilder(
|
||||||
context,
|
context,
|
||||||
attachment,
|
nonOGAttachments,
|
||||||
|
onAttachmentRemovePressed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (attachment.type) {
|
// Otherwise, use the default attachment list builder.
|
||||||
case 'image':
|
return LimitedBox(
|
||||||
case 'giphy':
|
maxHeight: 240,
|
||||||
return attachment.file != null
|
child: StreamMessageInputAttachmentList(
|
||||||
? Image.memory(
|
attachments: nonOGAttachments,
|
||||||
attachment.file!.bytes!,
|
onRemovePressed: onAttachmentRemovePressed,
|
||||||
fit: BoxFit.cover,
|
fileAttachmentListBuilder: widget.fileAttachmentListBuilder,
|
||||||
errorBuilder: (context, _, __) => Image.asset(
|
mediaAttachmentListBuilder: widget.mediaAttachmentListBuilder,
|
||||||
'images/placeholder.png',
|
fileAttachmentBuilder: widget.fileAttachmentBuilder,
|
||||||
package: 'stream_chat_flutter',
|
mediaAttachmentBuilder: widget.mediaAttachmentBuilder ??
|
||||||
),
|
// For backward compatibility.
|
||||||
)
|
// TODO: Remove in the next major release.
|
||||||
: CachedNetworkImage(
|
(context, attachment, onRemovePressed) {
|
||||||
imageUrl: attachment.imageUrl ??
|
final Widget mediaAttachmentThumbnail;
|
||||||
attachment.assetUrl ??
|
|
||||||
attachment.thumbUrl!,
|
final builder =
|
||||||
fit: BoxFit.cover,
|
widget.attachmentThumbnailBuilders?[attachment.type];
|
||||||
errorWidget: (_, obj, trace) =>
|
if (builder != null) {
|
||||||
getFileTypeImage(attachment.extraData['other'] as String?),
|
mediaAttachmentThumbnail = builder(context, attachment);
|
||||||
placeholder: (context, _) => Shimmer.fromColors(
|
} else {
|
||||||
baseColor: _streamChatTheme.colorTheme.disabled,
|
mediaAttachmentThumbnail = MessageInputMediaAttachmentThumbnail(
|
||||||
highlightColor: _streamChatTheme.colorTheme.inputBg,
|
attachment: attachment,
|
||||||
child: Image.asset(
|
);
|
||||||
'images/placeholder.png',
|
}
|
||||||
fit: BoxFit.cover,
|
|
||||||
package: 'stream_chat_flutter',
|
return ClipRRect(
|
||||||
),
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: mediaAttachmentThumbnail,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
},
|
||||||
return Stack(
|
),
|
||||||
children: [
|
);
|
||||||
StreamVideoThumbnailImage(
|
|
||||||
constraints: BoxConstraints.loose(
|
|
||||||
const Size(
|
|
||||||
104,
|
|
||||||
104,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
video: attachment.file?.path ?? attachment.assetUrl,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 8,
|
|
||||||
bottom: 10,
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
'svgs/video_call_icon.svg',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return const ColoredBox(
|
|
||||||
color: Colors.black26,
|
|
||||||
child: Icon(Icons.insert_drive_file),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton(BuildContext context) {
|
Widget _buildCommandButton(BuildContext context) {
|
||||||
|
|||||||
+388
@@ -0,0 +1,388 @@
|
|||||||
|
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/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';
|
||||||
|
|
||||||
|
/// WidgetBuilder used to build the message input attachment list.
|
||||||
|
///
|
||||||
|
/// see more:
|
||||||
|
/// - [StreamMessageInputAttachmentList]
|
||||||
|
typedef AttachmentListBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<Attachment> attachments,
|
||||||
|
ValueSetter<Attachment>? onRemovePressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// WidgetBuilder used to build the message input attachment item.
|
||||||
|
///
|
||||||
|
/// see more:
|
||||||
|
/// - [StreamMessageInputAttachmentList]
|
||||||
|
typedef AttachmentItemBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Attachment attachment,
|
||||||
|
ValueSetter<Attachment>? onRemovePressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template stream_message_input_attachment_list}
|
||||||
|
/// Widget used to display the list of attachments added to the message input.
|
||||||
|
///
|
||||||
|
/// By default, it displays the list of file attachments and media attachments
|
||||||
|
/// separately.
|
||||||
|
///
|
||||||
|
/// You can customize the list of file attachments and media attachments using
|
||||||
|
/// [fileAttachmentListBuilder] and [mediaAttachmentListBuilder] respectively.
|
||||||
|
///
|
||||||
|
/// You can also customize the attachment item using [fileAttachmentBuilder] and
|
||||||
|
/// [mediaAttachmentBuilder] respectively.
|
||||||
|
///
|
||||||
|
/// You can override the default action of removing an attachment by providing
|
||||||
|
/// [onRemovePressed].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class StreamMessageInputAttachmentList extends StatefulWidget {
|
||||||
|
/// {@macro stream_message_input_attachment_list}
|
||||||
|
const StreamMessageInputAttachmentList({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.onRemovePressed,
|
||||||
|
this.fileAttachmentBuilder,
|
||||||
|
this.mediaAttachmentBuilder,
|
||||||
|
this.fileAttachmentListBuilder,
|
||||||
|
this.mediaAttachmentListBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of attachments to display thumbnails for.
|
||||||
|
///
|
||||||
|
/// Open graph should be filtered out.
|
||||||
|
final Iterable<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment item.
|
||||||
|
final AttachmentItemBuilder? fileAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment item.
|
||||||
|
final AttachmentItemBuilder? mediaAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment list.
|
||||||
|
final AttachmentListBuilder? fileAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment list.
|
||||||
|
final AttachmentListBuilder? mediaAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StreamMessageInputAttachmentList> createState() =>
|
||||||
|
_StreamMessageInputAttachmentListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StreamMessageInputAttachmentListState
|
||||||
|
extends State<StreamMessageInputAttachmentList> {
|
||||||
|
List<Attachment> fileAttachments = [];
|
||||||
|
List<Attachment> mediaAttachments = [];
|
||||||
|
|
||||||
|
void _updateAttachments() {
|
||||||
|
// Clear the lists.
|
||||||
|
fileAttachments.clear();
|
||||||
|
mediaAttachments.clear();
|
||||||
|
|
||||||
|
// Split the attachments into file and media attachments.
|
||||||
|
for (final attachment in widget.attachments) {
|
||||||
|
if (attachment.type == 'file') {
|
||||||
|
fileAttachments.add(attachment);
|
||||||
|
} else {
|
||||||
|
mediaAttachments.add(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateAttachments();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant StreamMessageInputAttachmentList oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.attachments != widget.attachments) {
|
||||||
|
_updateAttachments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If there are no attachments, return an empty box.
|
||||||
|
if (fileAttachments.isEmpty && mediaAttachments.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (mediaAttachments.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: widget.mediaAttachmentListBuilder?.call(
|
||||||
|
context,
|
||||||
|
mediaAttachments,
|
||||||
|
widget.onRemovePressed,
|
||||||
|
) ??
|
||||||
|
MessageInputMediaAttachments(
|
||||||
|
attachments: mediaAttachments,
|
||||||
|
attachmentBuilder: widget.mediaAttachmentBuilder,
|
||||||
|
onRemovePressed: widget.onRemovePressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (fileAttachments.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: widget.fileAttachmentListBuilder?.call(
|
||||||
|
context,
|
||||||
|
fileAttachments,
|
||||||
|
widget.onRemovePressed,
|
||||||
|
) ??
|
||||||
|
MessageInputFileAttachments(
|
||||||
|
attachments: fileAttachments,
|
||||||
|
attachmentBuilder: widget.fileAttachmentBuilder,
|
||||||
|
onRemovePressed: widget.onRemovePressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].insertBetween(
|
||||||
|
Divider(
|
||||||
|
height: 16,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
thickness: 1,
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget used to display the list of file type attachments added to the
|
||||||
|
/// message input.
|
||||||
|
class MessageInputFileAttachments extends StatelessWidget {
|
||||||
|
/// Creates a new FileAttachments widget.
|
||||||
|
const MessageInputFileAttachments({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.attachmentBuilder,
|
||||||
|
this.onRemovePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of file type attachments to display thumbnails for.
|
||||||
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the file type attachment item.
|
||||||
|
final AttachmentItemBuilder? attachmentBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
reverse: true,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
children: attachments.reversed.map<Widget>(
|
||||||
|
(attachment) {
|
||||||
|
// If a custom builder is provided, use it.
|
||||||
|
final builder = attachmentBuilder;
|
||||||
|
if (builder != null) {
|
||||||
|
return builder(context, attachment, onRemovePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the default builder.
|
||||||
|
return ClipRRect(
|
||||||
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: StreamFileAttachment(
|
||||||
|
message: Message(), // dummy message
|
||||||
|
attachment: attachment,
|
||||||
|
constraints: BoxConstraints.loose(Size(
|
||||||
|
MediaQuery.sizeOf(context).width * 0.65,
|
||||||
|
56,
|
||||||
|
)),
|
||||||
|
trailing: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed!(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).insertBetween(const SizedBox(height: 8)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget used to display the list of media type attachments added to the
|
||||||
|
/// message input.
|
||||||
|
class MessageInputMediaAttachments extends StatelessWidget {
|
||||||
|
/// Creates a new MediaAttachments widget.
|
||||||
|
const MessageInputMediaAttachments({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.attachmentBuilder,
|
||||||
|
this.onRemovePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of media type attachments to display thumbnails for.
|
||||||
|
///
|
||||||
|
/// Only attachments of type `image`, `video` and `giphy` are supported. Shows
|
||||||
|
/// a placeholder for other types of attachments.
|
||||||
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the media type attachment item.
|
||||||
|
final AttachmentItemBuilder? attachmentBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 104,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
children: attachments.map<Widget>(
|
||||||
|
(attachment) {
|
||||||
|
// If a custom builder is provided, use it.
|
||||||
|
final builder = attachmentBuilder;
|
||||||
|
if (builder != null) {
|
||||||
|
return builder(context, attachment, onRemovePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: MessageInputMediaAttachmentThumbnail(
|
||||||
|
attachment: attachment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed!(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).insertBetween(const SizedBox(width: 8)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: [
|
||||||
|
StreamVideoThumbnailImage(
|
||||||
|
video: attachment.file?.path ?? attachment.assetUrl,
|
||||||
|
),
|
||||||
|
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.
|
||||||
|
const RemoveAttachmentButton({super.key, this.onPressed});
|
||||||
|
|
||||||
|
/// Callback when the remove attachment button is pressed.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
final colorTheme = theme.colorTheme;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: RawMaterialButton(
|
||||||
|
elevation: 0,
|
||||||
|
focusElevation: 0,
|
||||||
|
hoverElevation: 0,
|
||||||
|
highlightElevation: 0,
|
||||||
|
onPressed: onPressed,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
fillColor: colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
|
child: StreamSvgIcon.close(
|
||||||
|
size: 24,
|
||||||
|
color: colorTheme.barsBg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ export 'src/message_input/attachment_picker/stream_attachment_picker_bottom_shee
|
|||||||
export 'src/message_input/countdown_button.dart';
|
export 'src/message_input/countdown_button.dart';
|
||||||
export 'src/message_input/enums.dart';
|
export 'src/message_input/enums.dart';
|
||||||
export 'src/message_input/stream_message_input.dart';
|
export 'src/message_input/stream_message_input.dart';
|
||||||
|
export 'src/message_input/stream_message_input_attachment_list.dart';
|
||||||
export 'src/message_input/stream_message_send_button.dart';
|
export 'src/message_input/stream_message_send_button.dart';
|
||||||
export 'src/message_input/stream_message_text_field.dart';
|
export 'src/message_input/stream_message_text_field.dart';
|
||||||
export 'src/message_list_view/message_details.dart';
|
export 'src/message_list_view/message_details.dart';
|
||||||
@@ -96,3 +97,4 @@ export 'src/utils/device_segmentation.dart';
|
|||||||
export 'src/utils/extensions.dart';
|
export 'src/utils/extensions.dart';
|
||||||
export 'src/utils/helpers.dart';
|
export 'src/utils/helpers.dart';
|
||||||
export 'src/utils/typedefs.dart';
|
export 'src/utils/typedefs.dart';
|
||||||
|
export 'src/video/video_thumbnail_image.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user