Skip to content

Commit 8000c79

Browse files
authored
Add inline add more tile for gallery permissions (#2553)
* Add inline add more tile for gallery permissions * Add unit test * minor improvements
1 parent a0e886b commit 8000c79

18 files changed

+240
-104
lines changed

packages/stream_chat_flutter/lib/src/localization/translations.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ class DefaultTranslations implements Translations {
734734
String get somethingWentWrongError => 'Something went wrong';
735735

736736
@override
737-
String get addMoreFilesLabel => 'Add more files';
737+
String get addMoreFilesLabel => 'Add more';
738738

739739
@override
740740
String get enablePhotoAndVideoAccessMessage =>

packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,6 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
9292

9393
return OptionDrawer(
9494
margin: .zero,
95-
actions: [
96-
if (isLimited)
97-
IconButton(
98-
color: colorScheme.accentPrimary,
99-
icon: const Icon(Icons.add_circle_outline_rounded),
100-
onPressed: () async {
101-
await PhotoManager.presentLimited();
102-
_controller.doInitialLoad();
103-
},
104-
),
105-
],
10695
child: Builder(
10796
builder: (context) {
10897
if (!isPermissionGranted) {
@@ -143,6 +132,14 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
143132
thumbnailFormat: widget.config.mediaThumbnailFormat,
144133
thumbnailQuality: widget.config.mediaThumbnailQuality,
145134
thumbnailScale: widget.config.mediaThumbnailScale,
135+
addMoreBuilder: isLimited
136+
? (context) => _AddMoreTile(
137+
onTap: () async {
138+
await PhotoManager.presentLimited();
139+
_controller.doInitialLoad();
140+
},
141+
)
142+
: null,
146143
itemBuilder: (context, mediaItems, index, defaultWidget) {
147144
final media = mediaItems[index];
148145
return defaultWidget.copyWith(
@@ -158,6 +155,50 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
158155
}
159156
}
160157

158+
class _AddMoreTile extends StatelessWidget {
159+
const _AddMoreTile({required this.onTap});
160+
161+
final VoidCallback onTap;
162+
163+
@override
164+
Widget build(BuildContext context) {
165+
final colorScheme = context.streamColorScheme;
166+
final textTheme = context.streamTextTheme;
167+
final spacing = context.streamSpacing;
168+
169+
return Material(
170+
color: colorScheme.backgroundSurfaceCard,
171+
child: InkWell(
172+
onTap: onTap,
173+
overlayColor: WidgetStateProperty.resolveWith((states) {
174+
if (states.contains(WidgetState.pressed)) return colorScheme.statePressed;
175+
if (states.contains(WidgetState.hovered)) return colorScheme.stateHover;
176+
if (states.contains(WidgetState.focused)) return colorScheme.stateFocused;
177+
return null;
178+
}),
179+
child: Column(
180+
mainAxisAlignment: MainAxisAlignment.center,
181+
children: [
182+
Icon(
183+
context.streamIcons.plusLarge,
184+
size: 20,
185+
color: colorScheme.textTertiary,
186+
),
187+
SizedBox(height: spacing.xs),
188+
Text(
189+
context.translations.addMoreFilesLabel,
190+
style: textTheme.captionEmphasis.copyWith(
191+
color: colorScheme.textTertiary,
192+
),
193+
textAlign: TextAlign.center,
194+
),
195+
],
196+
),
197+
),
198+
);
199+
}
200+
}
201+
161202
/// Configuration for the [StreamGalleryPicker].
162203
class GalleryPickerConfig {
163204
/// Creates a [GalleryPickerConfig] instance.

packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart

Lines changed: 3 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -267,102 +267,30 @@ class _EndOfFrameCallbackWidgetState extends State<EndOfFrameCallbackWidget> {
267267
}
268268
}
269269

270-
const _kDefaultOptionDrawerShape = RoundedRectangleBorder(
271-
borderRadius: BorderRadius.only(
272-
topLeft: Radius.circular(16),
273-
topRight: Radius.circular(16),
274-
),
275-
);
276-
277270
/// A widget that will be shown in the attachment picker.
278271
/// It can be used to show a custom view for each attachment picker option.
279272
class OptionDrawer extends StatelessWidget {
280273
/// Creates a widget that will be shown in the attachment picker.
281274
const OptionDrawer({
282275
super.key,
283276
required this.child,
284-
this.color,
285-
this.elevation = 2,
286277
this.margin,
287-
this.clipBehavior = Clip.hardEdge,
288-
this.shape = _kDefaultOptionDrawerShape,
289-
this.title,
290-
this.actions = const [],
291278
});
292279

293280
/// The widget below this widget in the tree.
294281
final Widget child;
295282

296-
/// The background color of the options card.
297-
///
298-
/// Defaults to [StreamColorTheme.barsBg].
299-
final Color? color;
300-
301-
/// The elevation of the options card.
302-
///
303-
/// The default value is 2.
304-
final double elevation;
305-
306283
/// The margin of the options card.
307284
final EdgeInsetsGeometry? margin;
308285

309-
/// The clip behavior of the options card.
310-
///
311-
/// The default value is [Clip.hardEdge].
312-
final Clip clipBehavior;
313-
314-
/// The shape of the options card.
315-
final ShapeBorder shape;
316-
317-
/// The title of the options card.
318-
final Widget? title;
319-
320-
/// The actions available for the options card.
321-
final List<Widget> actions;
322-
323286
@override
324287
Widget build(BuildContext context) {
325-
var height = 0.0;
326-
if (title != null || actions.isNotEmpty) {
327-
height = 40.0;
328-
}
329-
330-
final leading = title ?? const Empty();
331-
332-
Widget trailing;
333-
if (actions.isNotEmpty) {
334-
trailing = Row(
335-
mainAxisSize: MainAxisSize.min,
336-
mainAxisAlignment: MainAxisAlignment.end,
337-
crossAxisAlignment: CrossAxisAlignment.stretch,
338-
children: actions,
339-
);
340-
} else {
341-
trailing = const Empty();
342-
}
343-
344288
final spacing = context.streamSpacing;
345289
final effectiveMargin = margin ?? .symmetric(horizontal: spacing.md, vertical: spacing.xxxl);
346290

347-
return Column(
348-
mainAxisSize: MainAxisSize.min,
349-
children: [
350-
SizedBox(
351-
height: height,
352-
child: Row(
353-
children: [
354-
Expanded(child: leading),
355-
Expanded(child: trailing),
356-
],
357-
),
358-
),
359-
Expanded(
360-
child: Container(
361-
margin: effectiveMargin,
362-
child: child,
363-
),
364-
),
365-
],
291+
return Container(
292+
margin: effectiveMargin,
293+
child: child,
366294
);
367295
}
368296
}

packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class StreamPhotoGallery extends StatelessWidget {
5656
this.thumbnailFormat = ThumbnailFormat.jpeg,
5757
this.thumbnailQuality = 100,
5858
this.thumbnailScale = 1,
59+
this.addMoreBuilder,
5960
});
6061

6162
/// The [StreamPhotoGalleryController] used to control the grid of users.
@@ -307,6 +308,11 @@ class StreamPhotoGallery extends StatelessWidget {
307308
/// Scale of the image.
308309
final double thumbnailScale;
309310

311+
/// An optional builder for a leading "Add more" tile shown as the first item
312+
/// in the gallery grid. Useful when the user has limited photo library access
313+
/// and needs a way to expand the selection.
314+
final WidgetBuilder? addMoreBuilder;
315+
310316
@override
311317
Widget build(BuildContext context) {
312318
return PagedValueGridView<int, AssetEntity>(
@@ -328,6 +334,7 @@ class StreamPhotoGallery extends StatelessWidget {
328334
restorationId: restorationId,
329335
clipBehavior: clipBehavior,
330336
loadMoreTriggerIndex: loadMoreTriggerIndex,
337+
leadingItemBuilder: addMoreBuilder,
331338
gridDelegate: gridDelegate,
332339
itemBuilder: (context, mediaList, index) {
333340
final media = mediaList[index];

packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ class PagedValueGridView<K, V> extends StatefulWidget {
365365
required this.loadingBuilder,
366366
required this.errorBuilder,
367367
this.loadMoreTriggerIndex = 3,
368+
this.leadingItemBuilder,
368369
this.scrollDirection = Axis.vertical,
369370
this.reverse = false,
370371
this.scrollController,
@@ -413,6 +414,13 @@ class PagedValueGridView<K, V> extends StatefulWidget {
413414
/// The index to take into account when triggering [controller.loadMore].
414415
final int loadMoreTriggerIndex;
415416

417+
/// An optional builder for a single item prepended before the paged items.
418+
///
419+
/// When provided, [itemBuilder] still receives regular item indices starting
420+
/// at 0 — the leading item is handled separately, similar to
421+
/// [loadMoreIndicatorBuilder].
422+
final WidgetBuilder? leadingItemBuilder;
423+
416424
/// {@template flutter.widgets.scroll_view.scrollDirection}
417425
/// The axis along which the scroll view scrolls.
418426
///
@@ -665,13 +673,18 @@ class _PagedValueGridViewState<K, V> extends State<PagedValueGridView<K, V>> {
665673
keyboardDismissBehavior: widget.keyboardDismissBehavior,
666674
restorationId: widget.restorationId,
667675
clipBehavior: widget.clipBehavior,
668-
itemCount: value.itemCount,
676+
itemCount: value.itemCount + (widget.leadingItemBuilder != null ? 1 : 0),
669677
gridDelegate: widget.gridDelegate,
670678
itemBuilder: (context, index) {
679+
var adjustedIndex = index;
680+
if (widget.leadingItemBuilder != null) {
681+
if (index == 0) return widget.leadingItemBuilder!(context);
682+
adjustedIndex = index - 1;
683+
}
684+
671685
if (!_hasRequestedNextPage) {
672686
final newPageRequestTriggerIndex = items.length - widget.loadMoreTriggerIndex;
673-
final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex;
674-
if (nextPageKey != null && isBuildingTriggerIndexItem) {
687+
if (nextPageKey != null && adjustedIndex == newPageRequestTriggerIndex) {
675688
// Schedules the request for the end of this frame.
676689
WidgetsBinding.instance.addPostFrameCallback((_) async {
677690
if (error == null) {
@@ -683,14 +696,14 @@ class _PagedValueGridViewState<K, V> extends State<PagedValueGridView<K, V>> {
683696
}
684697
}
685698

686-
if (index == items.length) {
699+
if (adjustedIndex == items.length) {
687700
if (error != null) {
688701
return widget.loadMoreErrorBuilder(context, error);
689702
}
690703
return widget.loadMoreIndicatorBuilder(context);
691704
}
692705

693-
return widget.itemBuilder(context, items, index);
706+
return widget.itemBuilder(context, items, adjustedIndex);
694707
},
695708
);
696709
},

0 commit comments

Comments
 (0)