Merge pull request #1631 from GetStream/feat/message-input-attachment-builder
This commit is contained in:
@@ -7,6 +7,26 @@
|
||||
- [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error
|
||||
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
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
/// {@template streamVideoAttachment}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.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';
|
||||
|
||||
/// {@template streamGalleryFooter}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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/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:video_player/video_player.dart';
|
||||
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart'
|
||||
hide ErrorListener;
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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/src/message_input/attachment_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/simple_safe_area.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';
|
||||
|
||||
const _kCommandTrigger = '/';
|
||||
@@ -110,6 +105,12 @@ class StreamMessageInput extends StatefulWidget {
|
||||
this.messageInputController,
|
||||
this.actions = const [],
|
||||
this.actionsLocation = ActionsLocation.left,
|
||||
this.attachmentListBuilder,
|
||||
this.fileAttachmentListBuilder,
|
||||
this.mediaAttachmentListBuilder,
|
||||
this.fileAttachmentBuilder,
|
||||
this.mediaAttachmentBuilder,
|
||||
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.focusNode,
|
||||
this.sendButtonLocation = SendButtonLocation.outside,
|
||||
@@ -130,6 +131,7 @@ class StreamMessageInput extends StatefulWidget {
|
||||
this.mentionAllAppUsers = false,
|
||||
this.sendButtonBuilder,
|
||||
this.quotedMessageBuilder,
|
||||
this.quotedMessageAttachmentThumbnailBuilders,
|
||||
this.shouldKeepFocusAfterMessage,
|
||||
this.validator = _defaultValidator,
|
||||
this.restorationId,
|
||||
@@ -209,9 +211,41 @@ class StreamMessageInput extends StatefulWidget {
|
||||
/// The location of the custom actions.
|
||||
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.
|
||||
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||
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.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
@@ -1147,193 +1181,93 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
||||
messageTheme: _streamChatTheme.otherMessageTheme,
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
||||
attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders,
|
||||
attachmentThumbnailBuilders:
|
||||
widget.quotedMessageAttachmentThumbnailBuilders,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachments() {
|
||||
final nonOGAttachments = _effectiveController.attachments.where(
|
||||
(it) => it.titleLink == null,
|
||||
);
|
||||
final attachments = _effectiveController.attachments;
|
||||
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();
|
||||
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) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
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;
|
||||
// Default callback for removing an attachment.
|
||||
Future<void> onAttachmentRemovePressed(Attachment attachment) async {
|
||||
final file = attachment.file;
|
||||
final uploadState = attachment.uploadState;
|
||||
|
||||
if (file != null && !uploadState.isSuccess && !isWeb) {
|
||||
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
||||
attachmentFile: file,
|
||||
);
|
||||
}
|
||||
if (file != null && !uploadState.isSuccess && !isWeb) {
|
||||
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
||||
attachmentFile: file,
|
||||
);
|
||||
}
|
||||
|
||||
_effectiveController.removeAttachmentById(attachment.id);
|
||||
},
|
||||
fillColor:
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
child: Center(
|
||||
child: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_effectiveController.removeAttachmentById(attachment.id);
|
||||
}
|
||||
|
||||
Widget _buildAttachment(Attachment attachment) {
|
||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
||||
true) {
|
||||
return widget.attachmentThumbnailBuilders![attachment.type!]!(
|
||||
// If the user has provided a custom attachment list builder, use that.
|
||||
final attachmentListBuilder = widget.attachmentListBuilder;
|
||||
if (attachmentListBuilder != null) {
|
||||
return attachmentListBuilder(
|
||||
context,
|
||||
attachment,
|
||||
nonOGAttachments,
|
||||
onAttachmentRemovePressed,
|
||||
);
|
||||
}
|
||||
|
||||
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) =>
|
||||
getFileTypeImage(attachment.extraData['other'] as String?),
|
||||
placeholder: (context, _) => Shimmer.fromColors(
|
||||
baseColor: _streamChatTheme.colorTheme.disabled,
|
||||
highlightColor: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
// Otherwise, use the default attachment list builder.
|
||||
return LimitedBox(
|
||||
maxHeight: 240,
|
||||
child: StreamMessageInputAttachmentList(
|
||||
attachments: nonOGAttachments,
|
||||
onRemovePressed: onAttachmentRemovePressed,
|
||||
fileAttachmentListBuilder: widget.fileAttachmentListBuilder,
|
||||
mediaAttachmentListBuilder: widget.mediaAttachmentListBuilder,
|
||||
fileAttachmentBuilder: widget.fileAttachmentBuilder,
|
||||
mediaAttachmentBuilder: widget.mediaAttachmentBuilder ??
|
||||
// For backward compatibility.
|
||||
// TODO: Remove in the next major release.
|
||||
(context, attachment, onRemovePressed) {
|
||||
final Widget mediaAttachmentThumbnail;
|
||||
|
||||
final builder =
|
||||
widget.attachmentThumbnailBuilders?[attachment.type];
|
||||
if (builder != null) {
|
||||
mediaAttachmentThumbnail = builder(context, attachment);
|
||||
} else {
|
||||
mediaAttachmentThumbnail = MessageInputMediaAttachmentThumbnail(
|
||||
attachment: attachment,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+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/enums.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_text_field.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/helpers.dart';
|
||||
export 'src/utils/typedefs.dart';
|
||||
export 'src/video/video_thumbnail_image.dart';
|
||||
|
||||
Reference in New Issue
Block a user