diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index a7d21c03..017bb06f 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -305,7 +305,8 @@ class Message extends Equatable { quotedMessage != null && quotedMessage is! _NullConst) { throw ArgumentError( - '`quotedMessage` can only be set as Message or null'); + '`quotedMessage` can only be set as Message or null', + ); } return true; }(), 'Validate type for quotedMessage'); @@ -315,7 +316,8 @@ class Message extends Equatable { quotedMessageId != null && quotedMessageId is! _NullConst) { throw ArgumentError( - '`quotedMessage` can only be set as String or null'); + '`quotedMessage` can only be set as String or null', + ); } return true; }(), 'Validate type for quotedMessage'); @@ -412,8 +414,8 @@ class Message extends Equatable { shadowed, silent, command, - createdAt, - updatedAt, + _createdAt, + _updatedAt, deletedAt, user, pinned, diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 60e8dd93..7eb68b79 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -756,7 +756,6 @@ void main() { const messageId = 'test-message-id'; final message = Message( id: messageId, - status: MessageSendingStatus.sending, ); expectLater( diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index c79ebeeb..258e4cc5 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,10 +1,16 @@ ## Upcoming +🛑️ Breaking Changes + +- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController` + +🐞 Fixed + +- Use file extension instead of mimeType for downloading files + ✅ Added -🛑️ Breaking Changes from `3.2.0` - -- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController` +- Videos can now be auto-played in `FullScreenMedia` ## 3.3.2 diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index fc0fb586..cc29f0b2 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -246,7 +246,8 @@ class GiphyAttachment extends AttachmentWidget { return StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 41b8d039..ff01eea3 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -141,7 +141,9 @@ class ImageAttachment extends AttachmentWidget { return StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: + message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index ce4ac804..0fc97d5e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -87,7 +87,9 @@ class VideoAttachment extends AttachmentWidget { builder: (_) => StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: + message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 0c10ae46..ac9213b6 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -365,12 +365,13 @@ class AttachmentActionsModal extends StatelessWidget { }) async { String? filePath; final appDocDir = await getTemporaryDirectory(); + final url = + attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!; await Dio().download( - attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!, + url, (Headers responseHeaders) { - final contentType = responseHeaders[Headers.contentTypeHeader]!; - final mimeType = contentType.first.split('/').last; - filePath ??= '${appDocDir.path}/${attachment.id}.$mimeType'; + final ext = Uri.parse(url).pathSegments.last; + filePath ??= '${appDocDir.path}/${attachment.id}.$ext'; return filePath!; }, onReceiveProgress: progressCallback, diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 6547a93d..84ddb803 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -34,6 +34,7 @@ class FullScreenMedia extends StatefulWidget { String? userName, this.onShowMessage, this.attachmentActionsModalBuilder, + this.autoplayVideos = false, }) : userName = userName ?? '', super(key: key); @@ -57,6 +58,9 @@ class FullScreenMedia extends StatefulWidget { /// Use [defaultActionsModal.copyWith] to easily customize it final AttachmentActionsBuilder? attachmentActionsModalBuilder; + /// Auto-play videos when page is opened + final bool autoplayVideos; + @override _FullScreenMediaState createState() => _FullScreenMediaState(); } @@ -81,7 +85,8 @@ class _FullScreenMediaState extends State ); _pageController = PageController(initialPage: widget.startIndex); _currentPage = widget.startIndex; - for (final attachment in widget.mediaAttachments) { + for (var i = 0; i < widget.mediaAttachments.length; i++) { + final attachment = widget.mediaAttachments[i]; if (attachment.type != 'video') continue; final package = VideoPackage(attachment, showControls: true); videoPackages[attachment.id] = package; @@ -90,9 +95,21 @@ class _FullScreenMediaState extends State } Future initializePlayers() async { + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = widget.mediaAttachments[widget.startIndex]; + await Future.wait(videoPackages.values.map( (it) => it.initialize(), )); + + if (widget.autoplayVideos && currentAttachment.type == 'video') { + final package = videoPackages.values + .firstWhere((e) => e._attachment == currentAttachment); + package._chewieController?.play(); + } setState(() {}); // ignore: no-empty-block } @@ -109,6 +126,24 @@ class _FullScreenMediaState extends State setState(() { _currentPage = val; }); + + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = widget.mediaAttachments[val]; + + for (final e in videoPackages.values) { + if (e._attachment != currentAttachment) { + e._chewieController?.pause(); + } + } + + if (widget.autoplayVideos && + currentAttachment.type == 'video') { + final controller = videoPackages[currentAttachment.id]!; + controller._chewieController?.play(); + } }, itemBuilder: (context, index) { final attachment = widget.mediaAttachments[index]; @@ -243,15 +278,16 @@ class _FullScreenMediaState extends State class VideoPackage { /// Constructor for creating [VideoPackage] VideoPackage( - Attachment attachment, { + this._attachment, { bool showControls = false, bool autoInitialize = true, }) : _showControls = showControls, _autoInitialize = autoInitialize, - _videoPlayerController = attachment.localUri != null - ? VideoPlayerController.file(File.fromUri(attachment.localUri!)) - : VideoPlayerController.network(attachment.assetUrl!); + _videoPlayerController = _attachment.localUri != null + ? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) + : VideoPlayerController.network(_attachment.assetUrl!); + final Attachment _attachment; final bool _showControls; final bool _autoInitialize; final VideoPlayerController _videoPlayerController; diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 7f6a6f6e..909f9561 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -23,41 +23,8 @@ typedef CustomAttachmentIconBuilder = Widget Function( /// A widget that allows to pick an attachment. class StreamAttachmentPicker extends StatefulWidget { - /// True if the picker is open. - final bool isOpen; - - /// The picker size in height. - final double pickerSize; - - /// The [MessageInputController] linked to this picker. - final MessageInputController messageInputController; - - /// The limit of attachments that can be picked. - final int attachmentLimit; - - /// The callback for when the attachment limit is exceeded. - final AttachmentLimitExceedListener? onAttachmentLimitExceeded; - - final ValueChanged? onError; - final FilePickerCallback onFilePicked; - - /// Video quality to use when compressing the videos. - final VideoQuality compressedVideoQuality; - - /// Frame rate to use when compressing the videos. - final int compressedVideoFrameRate; - - /// Max attachment size in bytes: - /// - Defaults to 20 MB - /// - Do not set it if you're using our default CDN - final int maxAttachmentSize; - - /// The list of attachment types that can be picked. - final List allowedAttachmentTypes; - - /// The list of custom attachment types that can be picked. - final List customAttachmentTypes; - + /// Default constructor for [StreamAttachmentPicker] which creates the Stream + /// attachment picker widget. const StreamAttachmentPicker({ Key? key, required this.messageInputController, @@ -78,6 +45,46 @@ class StreamAttachmentPicker extends StatefulWidget { this.customAttachmentTypes = const [], }) : super(key: key); + /// True if the picker is open. + final bool isOpen; + + /// The picker size in height. + final double pickerSize; + + /// The [MessageInputController] linked to this picker. + final MessageInputController messageInputController; + + /// The limit of attachments that can be picked. + final int attachmentLimit; + + /// The callback for when the attachment limit is exceeded. + final AttachmentLimitExceedListener? onAttachmentLimitExceeded; + + /// Callback for when an error occurs in the attachment picker. + final ValueChanged? onError; + + /// Callback for when file is picked. + final FilePickerCallback onFilePicked; + + /// Video quality to use when compressing the videos. + final VideoQuality compressedVideoQuality; + + /// Frame rate to use when compressing the videos. + final int compressedVideoFrameRate; + + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN + final int maxAttachmentSize; + + /// The list of attachment types that can be picked. + final List allowedAttachmentTypes; + + /// The list of custom attachment types that can be picked. + final List customAttachmentTypes; + + /// Used to create a new copy of [StreamAttachmentPicker] with modified + /// properties. StreamAttachmentPicker copyWith({ Key? key, MessageInputController? messageInputController, @@ -334,8 +341,8 @@ class _StreamAttachmentPickerState extends State { onMediaSelected: (media) { if (messageInputController.attachments .any((e) => e.id == media.id)) { - setState(() => messageInputController.attachments - .removeWhere((e) => e.id == media.id)); + messageInputController + .removeAttachmentById(media.id); } else { _addAssetAttachment(media); } @@ -547,14 +554,22 @@ class _PickerWidgetState extends State<_PickerWidget> { } } +/// Class which holds data for a custom attachment type in the attachment picker class CustomAttachmentType { - String type; - CustomAttachmentIconBuilder iconBuilder; - WidgetBuilder pickerBuilder; - + /// Default constructor for creating a custom attachment for the attachment + /// picker. CustomAttachmentType({ required this.type, required this.iconBuilder, required this.pickerBuilder, }); + + /// Type name. + String type; + + /// Builds the icon in the attachment picker top row. + CustomAttachmentIconBuilder iconBuilder; + + /// Builds content in the attachment builder when icon is selected. + WidgetBuilder pickerBuilder; } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index e7d765d9..61466aaf 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -1,4 +1,4 @@ -// ignore_for_file: prefer-trailing-comma, cascade_invocations +// ignore_for_file: prefer-trailing-comma, cascade_invocations, lines_longer_than_80_chars import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; @@ -44,7 +44,8 @@ class StreamMessageTextField extends StatefulWidget { /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to /// [MaxLengthEnforcement.none]. - /// The text field enforces the length with a [LengthLimitingTextInputFormatter], + /// The text field enforces the length with a + /// [LengthLimitingTextInputFormatter], /// which is evaluated after the supplied [inputFormatters], if any. /// The [maxLength] value must be either null or greater than zero. /// @@ -633,7 +634,7 @@ class StreamMessageTextField extends StatefulWidget { defaultValue: null)); properties.add(DiagnosticsProperty( 'scrollPadding', scrollPadding, - defaultValue: const EdgeInsets.all(20.0))); + defaultValue: const EdgeInsets.all(20))); properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, @@ -702,7 +703,6 @@ class _StreamMessageTextFieldState extends State @override Widget build(BuildContext context) => TextField( - key: widget.key, controller: _effectiveController.textEditingController, onChanged: (newText) { _effectiveController.text = newText; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b39a2eea..a4e9d715 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -23,16 +23,16 @@ export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/mention_tile.dart'; export 'src/message_action.dart'; +export 'src/message_input/countdown_button.dart'; export 'src/message_input/message_input.dart'; +export 'src/message_input/stream_attachment_picker.dart'; +export 'src/message_input/stream_message_send_button.dart'; +export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; -export 'src/message_input/countdown_button.dart'; -export 'src/message_input/stream_attachment_picker.dart'; -export 'src/message_input/stream_message_send_button.dart'; -export 'src/message_input/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart index c8832f6f..3b711a85 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 9210ac49..cfd6f4ec 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; @@ -37,6 +38,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageWidget: const Text( 'test', @@ -196,6 +198,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageTheme: streamTheme.ownMessageTheme, ), @@ -242,6 +245,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageTheme: streamTheme.ownMessageTheme, ),