diff --git a/melos.yaml b/melos.yaml index 58d3456f9c..4f8f2dc052 100644 --- a/melos.yaml +++ b/melos.yaml @@ -52,7 +52,7 @@ command: # upper bounds once FlutterFire ships a fixed release. firebase_crashlytics: '>=5.2.0 <5.2.5' firebase_messaging: '>=16.0.0 <16.4.2' - file_picker: ^11.0.0 + file_picker: '>=12.0.0 <14.0.0' file_selector: ^1.1.0 app_badge_plus: ^1.3.2 flutter_local_notifications: ^21.0.0 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 282e59758c..93c51dcbc5 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -8,6 +8,8 @@ ⚠️ Changed +- Bumped `file_picker` to `>=12.0.0 <14.0.0`. +- `PlatformFileX.toAttachmentFile` and `PlatformFileX.toAttachment` are now async. - Video thumbnails now use `stream_thumbnail` on every platform, and the `thumblr` dependency is gone. - Linux builds now need the FFmpeg and libwebp development packages — on Debian/Ubuntu: @@ -16,8 +18,13 @@ - `AccessibleMessagePreviewFormatter.formatMessageSemanticsLabel` must now return the body without a speaker prefix when `channel` is omitted. An implementation that prefixes unconditionally makes a message row announce "You said, You: hello". - `StreamImageCDN.resolveUrl` now leaves a URL that already asks for a specific size alone, rather than replacing it with the size the layout computed. +⚠️ Deprecated + +- Deprecated `withData` and `withReadStream` on `StreamAttachmentHandler.pickFile` and `StreamFilePicker`; they no longer have any effect. + 🐞 Fixed +- Fixed `StreamAttachmentHandler.pickFile` throwing on an empty selection; it now returns `null`. - Fixed the package no longer compiling when `stream_core_flutter` adds an avatar size. The three switches mapping `StreamAvatarSize` and `StreamAvatarGroupSize` onto an indicator size, an inner avatar size and the number of initials were exhaustive, so a size added upstream broke the build here. They fall back to the largest size they know now, and `StreamAvatarSize.xlPlus` (64px), `StreamAvatarSize.xxxl` / `StreamAvatarGroupSize.xxxl` (104px) are mapped explicitly. - Fixed `StreamAttachmentHandler` throwing `UnimplementedError` on WebAssembly builds. - Fixed the gallery tab vanishing from the attachment picker when `allowedAttachmentPickerTypes` allowed images or videos but not both. It now stays available and lists only the allowed media. diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart index 4f4605c677..5b3c7c08ed 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart @@ -32,8 +32,8 @@ abstract class StreamAttachmentHandlerBase { List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, int compressionQuality = 0, - bool withData = true, - bool withReadStream = false, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withData = true, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withReadStream = false, bool lockParentWindow = true, }) { throw UnimplementedError('pickFile is not implemented'); diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart index db70fa7ed5..b11133825d 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart @@ -22,23 +22,22 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, int compressionQuality = 0, - bool withData = true, - bool withReadStream = false, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withData = true, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withReadStream = false, bool lockParentWindow = true, }) async { - final result = await FilePicker.pickFiles( + final result = await FilePicker.pickFile( dialogTitle: dialogTitle, initialDirectory: initialDirectory, type: type, allowedExtensions: allowedExtensions, onFileLoading: onFileLoading, compressionQuality: compressionQuality, - withData: withData, - withReadStream: withReadStream, - lockParentWindow: lockParentWindow, + windowsOptions: WindowsOptions(lockParentWindow: lockParentWindow), + linuxOptions: LinuxOptions(lockParentWindow: lockParentWindow), ); - return result?.files.first.toAttachment(type: type.toAttachmentType()); + return await result?.xFile.toAttachment(type: type.toAttachmentType()); } @override diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart index a7017caa87..814216648a 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -131,23 +131,22 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, int compressionQuality = 0, - bool withData = true, - bool withReadStream = false, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withData = true, + @Deprecated('Content is read on demand; this no longer has any effect.') bool withReadStream = false, bool lockParentWindow = true, }) async { - final result = await FilePicker.pickFiles( + final result = await FilePicker.pickFile( dialogTitle: dialogTitle, initialDirectory: initialDirectory, type: type, allowedExtensions: allowedExtensions, onFileLoading: onFileLoading, compressionQuality: compressionQuality, - withData: withData, - withReadStream: withReadStream, - lockParentWindow: lockParentWindow, + windowsOptions: WindowsOptions(lockParentWindow: lockParentWindow), + linuxOptions: LinuxOptions(lockParentWindow: lockParentWindow), ); - return result?.files.first.toAttachment(type: type.toAttachmentType()); + return await result?.xFile.toAttachment(type: type.toAttachmentType()); } @override diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart index d3f296eca6..cc98355d3a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart @@ -19,8 +19,8 @@ class StreamFilePicker extends StatelessWidget { this.allowedExtensions, this.onFileLoading, this.compressionQuality = 0, - this.withData = false, - this.withReadStream = false, + @Deprecated('Content is read on demand; this no longer has any effect.') this.withData = false, + @Deprecated('Content is read on demand; this no longer has any effect.') this.withReadStream = false, this.lockParentWindow = false, }); @@ -46,9 +46,11 @@ class StreamFilePicker extends StatelessWidget { final int compressionQuality; /// Whether to include the file data in the [Attachment]. + @Deprecated('Content is read on demand; this no longer has any effect.') final bool withData; /// Whether to include the file read stream in the [Attachment]. + @Deprecated('Content is read on demand; this no longer has any effect.') final bool withReadStream; /// Whether to lock the parent window when the file picker is open. @@ -69,8 +71,6 @@ class StreamFilePicker extends StatelessWidget { allowedExtensions: allowedExtensions, onFileLoading: onFileLoading, compressionQuality: compressionQuality, - withData: withData, - withReadStream: withReadStream, lockParentWindow: lockParentWindow, ); }); diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index a3cc755e77..43a824dad3 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -116,37 +116,10 @@ extension IterableExtension on Iterable { /// Useful extension for [PlatformFile] extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] - AttachmentFile get toAttachmentFile { - return AttachmentFile( - // Path is not supported on web. - path: CurrentPlatform.isWeb ? null : path, - name: name, - bytes: bytes, - size: size, - ); - } + Future get toAttachmentFile => xFile.toAttachmentFile; /// Converts the [PlatformFile] to a [Attachment]. - Attachment toAttachment({required String type}) { - final file = toAttachmentFile; - final extraDataMap = {}; - - final mimeType = file.mediaType?.mimeType; - - if (mimeType != null) { - extraDataMap['mime_type'] = mimeType; - } - - extraDataMap['file_size'] = file.size!; - - final attachment = Attachment( - file: file, - type: type, - extraData: extraDataMap, - ); - - return attachment; - } + Future toAttachment({required String type}) => xFile.toAttachment(type: type); } /// Useful extension for [XFile] diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 133fcdca12..f00dff7ff4 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: diacritic: ^0.1.6 dio: ^5.11.0 ezanimation: ^0.6.0 - file_picker: ^11.0.0 + file_picker: '>=12.0.0 <14.0.0' file_selector: ^1.1.0 flutter: sdk: flutter diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_pick_file_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_pick_file_test.dart new file mode 100644 index 0000000000..ff3ab0427d --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_pick_file_test.dart @@ -0,0 +1,112 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('StreamAttachmentHandler.pickFile returns null when the picker is closed without a selection', () async { + _useFilePicker(_FakeFilePickerPlatform()); + + final attachment = await StreamAttachmentHandler.instance.pickFile(); + + expect(attachment, isNull); + }); + + test('StreamAttachmentHandler.pickFile builds an attachment from the picked file', () async { + final bytes = Uint8List.fromList(utf8.encode('file_picker 12')); + _useFilePicker(_FakeFilePickerPlatform(pickedFile: _FakePlatformFile(bytes, name: 'notes.txt'))); + + final attachment = await StreamAttachmentHandler.instance.pickFile(); + + expect(attachment?.type, AttachmentType.file); + expect(attachment?.file?.name, 'notes.txt'); + expect(attachment?.file?.bytes, bytes); + expect(attachment?.fileSize, bytes.length); + }); + + test('StreamAttachmentHandler.pickFile locks the parent window on Windows and Linux by default', () async { + final platform = _FakeFilePickerPlatform(); + _useFilePicker(platform); + + await StreamAttachmentHandler.instance.pickFile(); + + expect(platform.windowsOptions?.lockParentWindow, isTrue); + expect(platform.linuxOptions?.lockParentWindow, isTrue); + }); + + test('PlatformFileX.toAttachment builds an attachment of the given type from the file content', () async { + final bytes = Uint8List.fromList(utf8.encode('file_picker 12')); + final file = _FakePlatformFile(bytes, name: 'notes.txt'); + + final attachment = await file.toAttachment(type: AttachmentType.file); + + expect(attachment.type, AttachmentType.file); + expect(attachment.file?.bytes, bytes); + expect(attachment.mimeType, 'text/plain'); + expect(attachment.fileSize, bytes.length); + }); +} + +void _useFilePicker(FilePickerPlatform platform) { + final previous = FilePickerPlatform.instance; + FilePickerPlatform.instance = platform; + addTearDown(() => FilePickerPlatform.instance = previous); +} + +class _FakeFilePickerPlatform extends FilePickerPlatform { + _FakeFilePickerPlatform({this.pickedFile}); + + final PlatformFile? pickedFile; + + WindowsOptions? windowsOptions; + LinuxOptions? linuxOptions; + + @override + Future pickFile({ + String? dialogTitle, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + Function(FilePickerStatus)? onFileLoading, + int compressionQuality = 0, + AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), + WindowsOptions windowsOptions = const WindowsOptions(), + LinuxOptions linuxOptions = const LinuxOptions(), + WebOptions webOptions = const WebOptions(), + }) async { + this.windowsOptions = windowsOptions; + this.linuxOptions = linuxOptions; + return pickedFile; + } +} + +final class _FakePlatformFile extends PlatformFile { + _FakePlatformFile(this._bytes, {required this.name}); + + final Uint8List _bytes; + + @override + final String name; + + @override + Uri get uri => Uri.file('/picked/$name'); + + @override + XFile get xFile => XFile.fromData(_bytes, name: name, path: uri.toFilePath()); + + @override + int? lengthSync() => _bytes.length; + + @override + Future length() async => _bytes.length; + + @override + Future readAsBytes() async => _bytes; + + @override + Stream readAsByteStream() => Stream.value(_bytes); +} diff --git a/sample_app/android/build.gradle b/sample_app/android/build.gradle index 22f412dfaa..24ea932d7a 100644 --- a/sample_app/android/build.gradle +++ b/sample_app/android/build.gradle @@ -25,6 +25,10 @@ subprojects { } +subprojects { + project.evaluationDependsOn(":app") +} + tasks.register("clean", Delete) { delete rootProject.layout.buildDirectory } \ No newline at end of file