diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 2ed97aa95..33203c443 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -734,7 +734,7 @@ class DefaultTranslations implements Translations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart index aab105094..b5a4e3cd4 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -92,17 +92,6 @@ class _StreamGalleryPickerState extends State { return OptionDrawer( margin: .zero, - actions: [ - if (isLimited) - IconButton( - color: colorScheme.accentPrimary, - icon: const Icon(Icons.add_circle_outline_rounded), - onPressed: () async { - await PhotoManager.presentLimited(); - _controller.doInitialLoad(); - }, - ), - ], child: Builder( builder: (context) { if (!isPermissionGranted) { @@ -143,6 +132,14 @@ class _StreamGalleryPickerState extends State { thumbnailFormat: widget.config.mediaThumbnailFormat, thumbnailQuality: widget.config.mediaThumbnailQuality, thumbnailScale: widget.config.mediaThumbnailScale, + addMoreBuilder: isLimited + ? (context) => _AddMoreTile( + onTap: () async { + await PhotoManager.presentLimited(); + _controller.doInitialLoad(); + }, + ) + : null, itemBuilder: (context, mediaItems, index, defaultWidget) { final media = mediaItems[index]; return defaultWidget.copyWith( @@ -158,6 +155,50 @@ class _StreamGalleryPickerState extends State { } } +class _AddMoreTile extends StatelessWidget { + const _AddMoreTile({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Material( + color: colorScheme.backgroundSurfaceCard, + child: InkWell( + onTap: onTap, + overlayColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) return colorScheme.statePressed; + if (states.contains(WidgetState.hovered)) return colorScheme.stateHover; + if (states.contains(WidgetState.focused)) return colorScheme.stateFocused; + return null; + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + context.streamIcons.plusLarge, + size: 20, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.addMoreFilesLabel, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textTertiary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} + /// Configuration for the [StreamGalleryPicker]. class GalleryPickerConfig { /// Creates a [GalleryPickerConfig] instance. diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart index aec668176..365fe4829 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart @@ -267,13 +267,6 @@ class _EndOfFrameCallbackWidgetState extends State { } } -const _kDefaultOptionDrawerShape = RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), -); - /// A widget that will be shown in the attachment picker. /// It can be used to show a custom view for each attachment picker option. class OptionDrawer extends StatelessWidget { @@ -281,88 +274,23 @@ class OptionDrawer extends StatelessWidget { const OptionDrawer({ super.key, required this.child, - this.color, - this.elevation = 2, this.margin, - this.clipBehavior = Clip.hardEdge, - this.shape = _kDefaultOptionDrawerShape, - this.title, - this.actions = const [], }); /// The widget below this widget in the tree. final Widget child; - /// The background color of the options card. - /// - /// Defaults to [StreamColorTheme.barsBg]. - final Color? color; - - /// The elevation of the options card. - /// - /// The default value is 2. - final double elevation; - /// The margin of the options card. final EdgeInsetsGeometry? margin; - /// The clip behavior of the options card. - /// - /// The default value is [Clip.hardEdge]. - final Clip clipBehavior; - - /// The shape of the options card. - final ShapeBorder shape; - - /// The title of the options card. - final Widget? title; - - /// The actions available for the options card. - final List actions; - @override Widget build(BuildContext context) { - var height = 0.0; - if (title != null || actions.isNotEmpty) { - height = 40.0; - } - - final leading = title ?? const Empty(); - - Widget trailing; - if (actions.isNotEmpty) { - trailing = Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: actions, - ); - } else { - trailing = const Empty(); - } - final spacing = context.streamSpacing; final effectiveMargin = margin ?? .symmetric(horizontal: spacing.md, vertical: spacing.xxxl); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: height, - child: Row( - children: [ - Expanded(child: leading), - Expanded(child: trailing), - ], - ), - ), - Expanded( - child: Container( - margin: effectiveMargin, - child: child, - ), - ), - ], + return Container( + margin: effectiveMargin, + child: child, ); } } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart index c8245ee7c..fdfd16473 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart @@ -56,6 +56,7 @@ class StreamPhotoGallery extends StatelessWidget { this.thumbnailFormat = ThumbnailFormat.jpeg, this.thumbnailQuality = 100, this.thumbnailScale = 1, + this.addMoreBuilder, }); /// The [StreamPhotoGalleryController] used to control the grid of users. @@ -307,6 +308,11 @@ class StreamPhotoGallery extends StatelessWidget { /// Scale of the image. final double thumbnailScale; + /// An optional builder for a leading "Add more" tile shown as the first item + /// in the gallery grid. Useful when the user has limited photo library access + /// and needs a way to expand the selection. + final WidgetBuilder? addMoreBuilder; + @override Widget build(BuildContext context) { return PagedValueGridView( @@ -328,6 +334,7 @@ class StreamPhotoGallery extends StatelessWidget { restorationId: restorationId, clipBehavior: clipBehavior, loadMoreTriggerIndex: loadMoreTriggerIndex, + leadingItemBuilder: addMoreBuilder, gridDelegate: gridDelegate, itemBuilder: (context, mediaList, index) { final media = mediaList[index]; diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart index ea5fe13b0..ce60e68bb 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart @@ -365,6 +365,7 @@ class PagedValueGridView extends StatefulWidget { required this.loadingBuilder, required this.errorBuilder, this.loadMoreTriggerIndex = 3, + this.leadingItemBuilder, this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, @@ -413,6 +414,13 @@ class PagedValueGridView extends StatefulWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; + /// An optional builder for a single item prepended before the paged items. + /// + /// When provided, [itemBuilder] still receives regular item indices starting + /// at 0 — the leading item is handled separately, similar to + /// [loadMoreIndicatorBuilder]. + final WidgetBuilder? leadingItemBuilder; + /// {@template flutter.widgets.scroll_view.scrollDirection} /// The axis along which the scroll view scrolls. /// @@ -665,13 +673,18 @@ class _PagedValueGridViewState extends State> { keyboardDismissBehavior: widget.keyboardDismissBehavior, restorationId: widget.restorationId, clipBehavior: widget.clipBehavior, - itemCount: value.itemCount, + itemCount: value.itemCount + (widget.leadingItemBuilder != null ? 1 : 0), gridDelegate: widget.gridDelegate, itemBuilder: (context, index) { + var adjustedIndex = index; + if (widget.leadingItemBuilder != null) { + if (index == 0) return widget.leadingItemBuilder!(context); + adjustedIndex = index - 1; + } + if (!_hasRequestedNextPage) { final newPageRequestTriggerIndex = items.length - widget.loadMoreTriggerIndex; - final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; - if (nextPageKey != null && isBuildingTriggerIndexItem) { + if (nextPageKey != null && adjustedIndex == newPageRequestTriggerIndex) { // Schedules the request for the end of this frame. WidgetsBinding.instance.addPostFrameCallback((_) async { if (error == null) { @@ -683,14 +696,14 @@ class _PagedValueGridViewState extends State> { } } - if (index == items.length) { + if (adjustedIndex == items.length) { if (error != null) { return widget.loadMoreErrorBuilder(context, error); } return widget.loadMoreIndicatorBuilder(context); } - return widget.itemBuilder(context, items, index); + return widget.itemBuilder(context, items, adjustedIndex); }, ); }, diff --git a/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart b/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart new file mode 100644 index 000000000..c2877cdaf --- /dev/null +++ b/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +class _TestController extends PagedValueNotifier { + _TestController(List items, {int? nextPageKey}) : super(PagedValue(items: items, nextPageKey: nextPageKey)); + + @override + Future doInitialLoad() async {} + + @override + Future loadMore(int nextPageKey) async {} +} + +Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); + +PagedValueGridView _buildGrid( + _TestController controller, { + WidgetBuilder? leadingItemBuilder, + required List builtIndices, +}) { + return PagedValueGridView( + controller: controller, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + leadingItemBuilder: leadingItemBuilder, + itemBuilder: (context, items, index) { + builtIndices.add(index); + return Text('item-$index'); + }, + emptyBuilder: (_) => const Text('empty'), + loadMoreErrorBuilder: (_, __) => const Text('load-more-error'), + loadMoreIndicatorBuilder: (_) => const Text('load-more-indicator'), + loadingBuilder: (_) => const Text('loading'), + errorBuilder: (_, __) => const Text('error'), + ); +} + +void main() { + group('PagedValueGridView without leadingItemBuilder', () { + testWidgets('renders items starting at index 0', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget(_wrap(_buildGrid(controller, builtIndices: builtIndices))); + await tester.pump(); + + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('item-2'), findsOneWidget); + expect(builtIndices, [0, 1, 2]); + }); + + testWidgets('does not render a leading item', (tester) async { + final controller = _TestController(['a']); + final builtIndices = []; + + await tester.pumpWidget(_wrap(_buildGrid(controller, builtIndices: builtIndices))); + await tester.pump(); + + expect(find.text('leading'), findsNothing); + expect(builtIndices, [0]); + }); + }); + + group('PagedValueGridView with leadingItemBuilder', () { + testWidgets('renders the leading item before the paged items', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('item-2'), findsOneWidget); + }); + + testWidgets('itemBuilder receives item indices starting at 0, not offset', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(builtIndices, [0, 1, 2]); + }); + + testWidgets('renders leading item even with a single paged item', (tester) async { + final controller = _TestController(['a']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(builtIndices, [0]); + }); + + testWidgets('renders load-more indicator at correct position with leading item', (tester) async { + final controller = _TestController(['item-0', 'item-1'], nextPageKey: 1); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('load-more-indicator'), findsOneWidget); + expect(builtIndices, [0, 1]); + }); + }); +} diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index f62726096..36666ffdf 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -184,7 +184,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart index 683fd2283..d4b139fd3 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart @@ -165,7 +165,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Alguna cosa ha anat malament'; @override - String get addMoreFilesLabel => 'Afegir més fitxers'; + String get addMoreFilesLabel => 'Afegir més'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart index 973898439..01d25dd96 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart @@ -159,7 +159,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Etwas ist schief gelaufen'; @override - String get addMoreFilesLabel => 'Weitere Dateien hinzufügen'; + String get addMoreFilesLabel => 'Mehr hinzufügen'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 724b2031c..6d09eedef 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -164,7 +164,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index e2e243db5..8cf7c799b 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -165,7 +165,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Algo ha salido mal'; @override - String get addMoreFilesLabel => 'Añadir más archivos'; + String get addMoreFilesLabel => 'Añadir más'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 9b4c0653a..a91ea1ae8 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -165,7 +165,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Quelque chose a mal tourné'; @override - String get addMoreFilesLabel => "Ajouter d'autres fichiers"; + String get addMoreFilesLabel => 'Ajouter plus'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 4ec51bcda..756045a96 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -164,7 +164,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'लोड करने में समस्या'; @override - String get addMoreFilesLabel => 'और फ़ाइलें जोड़ें'; + String get addMoreFilesLabel => 'और जोड़ें'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index c76b60500..5bbed46af 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -169,7 +169,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get somethingWentWrongError => 'Qualcosa è andato storto'; @override - String get addMoreFilesLabel => 'Aggiungi altri file'; + String get addMoreFilesLabel => 'Aggiungi altri'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 2d48316de..2fa9abb65 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -158,7 +158,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'エラーが発生しました'; @override - String get addMoreFilesLabel => 'ファイルの追加'; + String get addMoreFilesLabel => 'さらに追加'; @override String get enablePhotoAndVideoAccessMessage => 'お友達と共有できるように、写真やビデオへのアクセスを有効にしてください。'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index d325b3a88..6269977a0 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -158,7 +158,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get somethingWentWrongError => '뭔가 잘못됐습느다'; @override - String get addMoreFilesLabel => '파일을 추가함'; + String get addMoreFilesLabel => '더 추가'; @override String get enablePhotoAndVideoAccessMessage => '친구와 공유할 수 있도록 사진과 동영상에 액세스할 수 있도록 설정하십시오.'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart index b6e22e4a3..a335042b0 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -160,7 +160,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Noe gikk galt'; @override - String get addMoreFilesLabel => 'Legg til flere filer'; + String get addMoreFilesLabel => 'Legg til flere'; @override String get enablePhotoAndVideoAccessMessage => diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 27a3e98ea..72530f80f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -162,7 +162,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Algo deu errado'; @override - String get addMoreFilesLabel => 'Adicionar mais arquivos'; + String get addMoreFilesLabel => 'Adicionar mais'; @override String get enablePhotoAndVideoAccessMessage =>