From 485e077596dc7edddcd4cdf28c432bcc517497d5 Mon Sep 17 00:00:00 2001 From: Reuben Turner Date: Thu, 15 Jul 2021 09:44:12 -0400 Subject: [PATCH] feat: add GalleryHeader/GalleryFooter theme classes (#546) * chore: Add ImageHeaderTheme and ImageHeaderThemeData to stream_chat_theme.dart This commit adds the following classes to `stream_chat_theme.dart`: 1. `ImageHeaderTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageHeaderThemeData` 2. `ImageHeaderThemeData`, a class that allows for the customization of `ImageHeader` widgets * Make ImageHeaderThemeData constructor const * test: initial ImageHeaderThemeData tests Tests the copyWith, ==, and hashcode functions * test: add tests for default ImageHeaderThemeData values Tests the default values of ImageHeaderThemeData that gets built in a StreamChatThemeData against control values. * Update test name and add comments describing control * fix: use ImageHeaderTheme.of(context) instead of StreamChatTheme.of(context) This ensures that ImageHeader will always look for the closest ImageHeaderThemeData in the widget tree. * test: add dark theme default values test * run dartfmt on test file * test: lerping between ImageHeaderThemeData Added tests that lerp from one ImageHeaderThemeData to another (light to dark and dark to light) * test: merging ImageHeaderThemeData Added tests that merge light and dark ImageHeaderThemeData together (light + dark = dark, dark + light = light) * test: add a half-lerp test Added a test that lerps halway between light and dark ImageHeaderThemeData * chore: Add ImageFooterTheme and ImageFooterThemeData to stream_chat_theme.dart This commit adds the following classes to `stream_chat_theme.dart`: 1. `ImageFooterTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageFooterThemeData` 2. `ImageFooterThemeData`, a class that allows for the customization of `ImageFooter` widget * chore: apply ImageFooterThemeData to image_footer.dart * fix: make ImageFooterThemeData constructor const * test: add tests for ImageFooterThemeData * add a missing trailing comma * chore: rename ImageHeader & ImageFooter to GalleryHeader and GalleryFooter The respective theme classes and tests have also been renamed * Run dartfmt on test * test: fix failing tests due to renaming --- .../lib/src/full_screen_media.dart | 8 +- ...{image_footer.dart => gallery_footer.dart} | 29 +- ...{image_header.dart => gallery_header.dart} | 20 +- .../lib/src/stream_chat_theme.dart | 499 ++++++++++++++++-- .../lib/stream_chat_flutter.dart | 4 +- .../test/src/gallery_footer_theme_test.dart | 185 +++++++ .../test/src/gallery_header_theme_test.dart | 190 +++++++ .../test/src/image_footer_test.dart | 2 +- 8 files changed, 874 insertions(+), 63 deletions(-) rename packages/stream_chat_flutter/lib/src/{image_footer.dart => gallery_footer.dart} (91%) rename packages/stream_chat_flutter/lib/src/{image_header.dart => gallery_header.dart} (83%) create mode 100644 packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart create mode 100644 packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart 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 f86ac60b..0f6101b9 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -6,8 +6,8 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:stream_chat_flutter/src/image_footer.dart'; -import 'package:stream_chat_flutter/src/image_header.dart'; +import 'package:stream_chat_flutter/src/gallery_footer.dart'; +import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; @@ -180,7 +180,7 @@ class _FullScreenMediaState extends State child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ImageHeader( + GalleryHeader( userName: widget.userName, sentAt: // ignore: lines_longer_than_80_chars @@ -198,7 +198,7 @@ class _FullScreenMediaState extends State }, ), if (widget.message.type != 'ephemeral') - ImageFooter( + GalleryFooter( currentPage: _currentPage, totalPages: widget.mediaAttachments.length, mediaAttachments: widget.mediaAttachments, diff --git a/packages/stream_chat_flutter/lib/src/image_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart similarity index 91% rename from packages/stream_chat_flutter/lib/src/image_footer.dart rename to packages/stream_chat_flutter/lib/src/gallery_footer.dart index 4b32afd8..c26c8dbc 100644 --- a/packages/stream_chat_flutter/lib/src/image_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -13,9 +13,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Footer widget for media display -class ImageFooter extends StatefulWidget implements PreferredSizeWidget { +class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Creates a channel header - const ImageFooter({ + const GalleryFooter({ Key? key, required this.message, this.onBackPressed, @@ -54,13 +54,13 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget { final ValueChanged? mediaSelectedCallBack; @override - _ImageFooterState createState() => _ImageFooterState(); + _GalleryFooterState createState() => _GalleryFooterState(); @override final Size preferredSize; } -class _ImageFooterState extends State { +class _GalleryFooterState extends State { final TextEditingController _messageController = TextEditingController(); final FocusNode _messageFocusNode = FocusNode(); @@ -78,7 +78,7 @@ class _ImageFooterState extends State { Widget build(BuildContext context) { const showShareButton = !kIsWeb; final mediaQueryData = MediaQuery.of(context); - final chatThemeData = StreamChatTheme.of(context); + final galleryFooterThemeData = GalleryFooterTheme.of(context); return SizedBox.fromSize( size: Size( mediaQueryData.size.width, @@ -88,7 +88,7 @@ class _ImageFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: chatThemeData.colorTheme.barsBg, + color: galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -100,7 +100,7 @@ class _ImageFooterState extends State { IconButton( icon: StreamSvgIcon.iconShare( size: 24, - color: chatThemeData.colorTheme.textHighEmphasis, + color: galleryFooterThemeData.shareIconColor, ), onPressed: () async { final attachment = @@ -136,7 +136,7 @@ class _ImageFooterState extends State { children: [ Text( '${widget.currentPage + 1} of ${widget.totalPages}', - style: chatThemeData.textTheme.headlineBold, + style: galleryFooterThemeData.titleTextStyle, ), ], ), @@ -144,7 +144,7 @@ class _ImageFooterState extends State { ), IconButton( icon: StreamSvgIcon.iconGrid( - color: chatThemeData.colorTheme.textHighEmphasis, + color: galleryFooterThemeData.gridIconButtonColor, ), onPressed: () => _showPhotosModal(context), ), @@ -157,10 +157,11 @@ class _ImageFooterState extends State { void _showPhotosModal(context) { final chatThemeData = StreamChatTheme.of(context); + final galleryFooterThemeData = GalleryFooterTheme.of(context); showModalBottomSheet( context: context, - barrierColor: chatThemeData.colorTheme.overlay, - backgroundColor: chatThemeData.colorTheme.barsBg, + barrierColor: galleryFooterThemeData.bottomSheetBarrierColor, + backgroundColor: galleryFooterThemeData.bottomSheetBackgroundColor, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -191,7 +192,8 @@ class _ImageFooterState extends State { padding: const EdgeInsets.all(16), child: Text( 'Photos', - style: chatThemeData.textTheme.headlineBold, + style: + galleryFooterThemeData.bottomSheetPhotosTextStyle, ), ), ), @@ -199,7 +201,8 @@ class _ImageFooterState extends State { alignment: Alignment.centerRight, child: IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.textHighEmphasis, + color: + galleryFooterThemeData.bottomSheetCloseIconColor, ), onPressed: () => Navigator.maybePop(context), ), diff --git a/packages/stream_chat_flutter/lib/src/image_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart similarity index 83% rename from packages/stream_chat_flutter/lib/src/image_header.dart rename to packages/stream_chat_flutter/lib/src/gallery_header.dart index 92f4fa10..6c4ecfee 100644 --- a/packages/stream_chat_flutter/lib/src/image_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -5,9 +5,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Header/AppBar widget for media display screen -class ImageHeader extends StatelessWidget implements PreferredSizeWidget { +class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Creates a channel header - const ImageHeader({ + const GalleryHeader({ Key? key, required this.message, this.currentIndex = 0, @@ -51,7 +51,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final galleryHeaderThemeData = GalleryHeaderTheme.of(context); return AppBar( textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, @@ -59,18 +59,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { leading: showBackButton ? IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.textHighEmphasis, + color: galleryHeaderThemeData.closeButtonColor, size: 24, ), onPressed: onBackPressed, ) : const SizedBox(), - backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: galleryHeaderThemeData.backgroundColor, actions: [ if (message.type != 'ephemeral') IconButton( icon: StreamSvgIcon.iconMenuPoint( - color: chatThemeData.colorTheme.textHighEmphasis, + color: galleryHeaderThemeData.iconMenuPointColor, ), onPressed: () { _showMessageActionModalBottomSheet(context); @@ -90,11 +90,11 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( userName, - style: chatThemeData.textTheme.headlineBold, + style: galleryHeaderThemeData.titleTextStyle, ), Text( sentAt, - style: chatThemeData.channelPreviewTheme.subtitle, + style: galleryHeaderThemeData.subtitleTextStyle, ), ], ), @@ -109,11 +109,13 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { void _showMessageActionModalBottomSheet(BuildContext context) async { final channel = StreamChannel.of(context).channel; + final galleryHeaderThemeData = + StreamChatTheme.of(context).galleryHeaderTheme; final result = await showDialog( useRootNavigator: false, context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor, builder: (context) => StreamChannel( channel: channel, child: AttachmentActionsModal( diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 72b04a80..498b429e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; @@ -58,6 +59,8 @@ class StreamChatThemeData { Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, + GalleryHeaderThemeData? imageHeaderTheme, + GalleryFooterThemeData? imageFooterTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -80,6 +83,8 @@ class StreamChatThemeData { defaultUserImage: defaultUserImage, primaryIconTheme: primaryIconTheme, reactionIcons: reactionIcons, + galleryHeaderTheme: imageHeaderTheme, + galleryFooterTheme: imageFooterTheme, ); return defaultData.merge(customizedData); @@ -107,6 +112,8 @@ class StreamChatThemeData { required this.defaultUserImage, required this.primaryIconTheme, required this.reactionIcons, + required this.galleryHeaderTheme, + required this.galleryFooterTheme, }); /// Create a theme from a Material [Theme] @@ -136,6 +143,14 @@ class StreamChatThemeData { /// Theme of the chat widgets dedicated to a channel final ChannelTheme channelTheme; + /// The default style for [GalleryHeader]s below the overall + /// [StreamChatTheme]. + final GalleryHeaderThemeData galleryHeaderTheme; + + /// The default style for [GalleryFooter]s below the overall + /// [StreamChatTheme]. + final GalleryFooterThemeData galleryFooterTheme; + /// Theme of the current user messages final MessageTheme ownMessageTheme; @@ -172,6 +187,8 @@ class StreamChatThemeData { IconThemeData? primaryIconTheme, ChannelListHeaderTheme? channelListHeaderTheme, List? reactionIcons, + GalleryHeaderThemeData? galleryHeaderTheme, + GalleryFooterThemeData? galleryFooterTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -188,6 +205,8 @@ class StreamChatThemeData { otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), messageInputTheme: this.messageInputTheme.merge(messageInputTheme), reactionIcons: reactionIcons ?? this.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, + galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, ); /// Merge themes @@ -207,6 +226,8 @@ class StreamChatThemeData { otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), messageInputTheme: messageInputTheme.merge(other.messageInputTheme), reactionIcons: other.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), + galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), ); } @@ -219,6 +240,40 @@ class StreamChatThemeData { final accentColor = colorTheme.accentPrimary; final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); + final channelTheme = ChannelTheme( + channelHeaderTheme: ChannelHeaderTheme( + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: colorTheme.barsBg, + title: textTheme.headlineBold, + subtitle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + ), + ); + final channelPreviewTheme = ChannelPreviewTheme( + unreadCounterColor: colorTheme.accentError, + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: textTheme.bodyBold, + subtitle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAt: textTheme.footnote.copyWith( + color: colorTheme.textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, + ); return StreamChatThemeData.raw( textTheme: textTheme, colorTheme: colorTheme, @@ -231,24 +286,7 @@ class StreamChatThemeData { fit: BoxFit.cover, ), ), - channelPreviewTheme: ChannelPreviewTheme( - unreadCounterColor: colorTheme.accentError, - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - title: textTheme.bodyBold, - subtitle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - lastMessageAt: textTheme.footnote.copyWith( - color: colorTheme.textHighEmphasis.withOpacity(.5), - ), - indicatorIconSize: 16, - ), + channelPreviewTheme: channelPreviewTheme, channelListHeaderTheme: ChannelListHeaderTheme( avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), @@ -260,22 +298,7 @@ class StreamChatThemeData { color: colorTheme.barsBg, title: textTheme.headlineBold, ), - channelTheme: ChannelTheme( - channelHeaderTheme: ChannelHeaderTheme( - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - color: colorTheme.barsBg, - title: textTheme.headlineBold, - subtitle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - ), - ), + channelTheme: channelTheme, ownMessageTheme: MessageTheme( messageAuthor: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), @@ -407,6 +430,24 @@ class StreamChatThemeData { }, ), ], + galleryHeaderTheme: GalleryHeaderThemeData( + closeButtonColor: colorTheme.textHighEmphasis, + backgroundColor: channelTheme.channelHeaderTheme.color, + iconMenuPointColor: colorTheme.textHighEmphasis, + titleTextStyle: textTheme.headlineBold, + subtitleTextStyle: channelPreviewTheme.subtitle, + bottomSheetBarrierColor: colorTheme.overlay, + ), + galleryFooterTheme: GalleryFooterThemeData( + backgroundColor: colorTheme.barsBg, + shareIconColor: colorTheme.textHighEmphasis, + titleTextStyle: textTheme.headlineBold, + gridIconButtonColor: colorTheme.textHighEmphasis, + bottomSheetBarrierColor: colorTheme.overlay, + bottomSheetBackgroundColor: colorTheme.barsBg, + bottomSheetPhotosTextStyle: textTheme.headlineBold, + bottomSheetCloseIconColor: colorTheme.textHighEmphasis, + ), ); } } @@ -1287,3 +1328,393 @@ class Effect { blur: blur ?? this.blur, ); } + +/// Overrides the default style of [GalleryHeader] descendants. +/// +/// See also: +/// +/// * [GalleryHeaderThemeData], which is used to configure this theme. +class GalleryHeaderTheme extends InheritedTheme { + /// Creates an [GalleryHeaderTheme]. + /// + /// The [data] parameter must not be null. + const GalleryHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// [StreamChatThemeData.galleryHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); + /// ``` + static GalleryHeaderThemeData of(BuildContext context) { + final galleryHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return galleryHeaderTheme?.data ?? + StreamChatTheme.of(context).galleryHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryHeader]s when used +/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryHeaderTheme]. +/// +/// See also: +/// +/// * [GalleryHeaderTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override +/// the default style for [GalleryHeader]s below the overall [StreamChatTheme]. +class GalleryHeaderThemeData with Diagnosticable { + /// Creates an [GalleryHeaderThemeData]. + const GalleryHeaderThemeData({ + this.closeButtonColor, + this.backgroundColor, + this.iconMenuPointColor, + this.titleTextStyle, + this.subtitleTextStyle, + this.bottomSheetBarrierColor, + }); + + /// The color of the "close" button. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? closeButtonColor; + + /// The background color of the [GalleryHeader] widget. + /// + /// Defaults to [ChannelHeaderTheme.color]. + final Color? backgroundColor; + + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? iconMenuPointColor; + + /// The [TextStyle] to use for the [GalleryHeader] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The [TextStyle] to use for the [GalleryHeader] subtitle text. + /// + /// Defaults to [ChannelPreviewTheme.subtitle]. + final TextStyle? subtitleTextStyle; + + /// + final Color? bottomSheetBarrierColor; + + /// Copies this [GalleryHeaderThemeData] to another. + GalleryHeaderThemeData copyWith({ + Color? closeButtonColor, + Color? backgroundColor, + Color? iconMenuPointColor, + TextStyle? titleTextStyle, + TextStyle? subtitleTextStyle, + Color? bottomSheetBarrierColor, + }) => + GalleryHeaderThemeData( + closeButtonColor: closeButtonColor ?? this.closeButtonColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + ); + + /// Linearly interpolate between two [GalleryHeader] themes. + /// + /// All the properties must be non-null. + GalleryHeaderThemeData lerp( + GalleryHeaderThemeData a, + GalleryHeaderThemeData b, + double t, + ) => + GalleryHeaderThemeData( + closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + iconMenuPointColor: + Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + subtitleTextStyle: + TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + ); + + /// Merges one [GalleryHeaderThemeData] with the another + GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { + if (other == null) return this; + return copyWith( + closeButtonColor: other.closeButtonColor, + backgroundColor: other.backgroundColor, + iconMenuPointColor: other.iconMenuPointColor, + titleTextStyle: other.titleTextStyle, + subtitleTextStyle: other.subtitleTextStyle, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryHeaderThemeData && + runtimeType == other.runtimeType && + closeButtonColor == other.closeButtonColor && + backgroundColor == other.backgroundColor && + iconMenuPointColor == other.iconMenuPointColor && + titleTextStyle == other.titleTextStyle && + subtitleTextStyle == other.subtitleTextStyle && + bottomSheetBarrierColor == other.bottomSheetBarrierColor; + + @override + int get hashCode => + closeButtonColor.hashCode ^ + backgroundColor.hashCode ^ + iconMenuPointColor.hashCode ^ + titleTextStyle.hashCode ^ + subtitleTextStyle.hashCode ^ + bottomSheetBarrierColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('closeButtonColor', closeButtonColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('iconMenuPointColor', iconMenuPointColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)); + } +} + +/// Overrides the default style of [GalleryFooter] descendants. +/// +/// See also: +/// +/// * [GalleryFooterThemeData], which is used to configure this theme. +class GalleryFooterTheme extends InheritedTheme { + /// Creates an [GalleryFooterTheme]. + /// + /// The [data] parameter must not be null. + const GalleryFooterTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryFooterThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryFooterTheme] widget, then + /// [StreamChatThemeData.galleryFooterTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageFooterTheme theme = ImageFooterTheme.of(context); + /// ``` + static GalleryFooterThemeData of(BuildContext context) { + final imageFooterTheme = + context.dependOnInheritedWidgetOfExactType(); + return imageFooterTheme?.data ?? + StreamChatTheme.of(context).galleryFooterTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryFooterTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryFooterTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryFooter]s when used +/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryFooterTheme]. +/// +/// See also: +/// +/// * [GalleryFooterTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override +/// the default style for [GalleryFooter]s below the overall [StreamChatTheme]. +class GalleryFooterThemeData with Diagnosticable { + /// Creates an [GalleryFooterThemeData]. + const GalleryFooterThemeData({ + this.backgroundColor, + this.shareIconColor, + this.titleTextStyle, + this.gridIconButtonColor, + this.bottomSheetBarrierColor, + this.bottomSheetBackgroundColor, + this.bottomSheetPhotosTextStyle, + this.bottomSheetCloseIconColor, + }); + + /// The background color for the [GalleryFooter] widget. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? backgroundColor; + + /// The color for the "share" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? shareIconColor; + + /// The [TextStyle] to use for the [GalleryFooter] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The color to use for the "grid" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? gridIconButtonColor; + + /// The color to use behind the bottom sheet. + /// + /// Defaults to [ColorTheme.overlay]. + final Color? bottomSheetBarrierColor; + + /// The background color to use for the bottom sheet. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? bottomSheetBackgroundColor; + + /// The [TextStyle] to use for the "photos" text in the bottom sheet. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? bottomSheetPhotosTextStyle; + + /// The color to use for the "close" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? bottomSheetCloseIconColor; + + /// Copies this [GalleryFooterThemeData] to another. + GalleryFooterThemeData copyWith({ + Color? backgroundColor, + Color? shareIconColor, + TextStyle? titleTextStyle, + Color? gridIconButtonColor, + Color? bottomSheetBarrierColor, + Color? bottomSheetBackgroundColor, + TextStyle? bottomSheetPhotosTextStyle, + Color? bottomSheetCloseIconColor, + }) => + GalleryFooterThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + shareIconColor: shareIconColor ?? this.shareIconColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBackgroundColor: + bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, + bottomSheetPhotosTextStyle: + bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, + bottomSheetCloseIconColor: + bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, + ); + + /// Linearly interpolate between two [GalleryFooter] themes. + /// + /// All the properties must be non-null. + GalleryFooterThemeData lerp( + GalleryFooterThemeData a, + GalleryFooterThemeData b, + double t, + ) => + GalleryFooterThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + gridIconButtonColor: + Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + bottomSheetBackgroundColor: Color.lerp( + a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t), + bottomSheetPhotosTextStyle: TextStyle.lerp( + a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t), + bottomSheetCloseIconColor: Color.lerp( + a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), + ); + + /// Merges one [GalleryFooterThemeData] with the another + GalleryFooterThemeData merge(GalleryFooterThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + bottomSheetBackgroundColor: other.bottomSheetBackgroundColor, + bottomSheetCloseIconColor: other.bottomSheetCloseIconColor, + bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle, + gridIconButtonColor: other.gridIconButtonColor, + titleTextStyle: other.titleTextStyle, + shareIconColor: other.shareIconColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryFooterThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor && + shareIconColor == other.shareIconColor && + titleTextStyle == other.titleTextStyle && + gridIconButtonColor == other.gridIconButtonColor && + bottomSheetBarrierColor == other.bottomSheetBarrierColor && + bottomSheetBackgroundColor == other.bottomSheetBackgroundColor && + bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle && + bottomSheetCloseIconColor == other.bottomSheetCloseIconColor; + + @override + int get hashCode => + backgroundColor.hashCode ^ + shareIconColor.hashCode ^ + titleTextStyle.hashCode ^ + gridIconButtonColor.hashCode ^ + bottomSheetBarrierColor.hashCode ^ + bottomSheetBackgroundColor.hashCode ^ + bottomSheetPhotosTextStyle.hashCode ^ + bottomSheetCloseIconColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('shareIconColor', shareIconColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) + ..add(ColorProperty( + 'bottomSheetBackgroundColor', bottomSheetBackgroundColor)) + ..add(DiagnosticsProperty( + 'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle)) + ..add(ColorProperty( + 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 62bfd299..45c0f32e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -12,8 +12,8 @@ export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; export 'src/full_screen_media.dart'; -export 'src/image_footer.dart'; -export 'src/image_header.dart'; +export 'src/gallery_footer.dart'; +export 'src/gallery_header.dart'; export 'src/info_tile.dart'; export 'src/mention_tile.dart'; export 'src/message_action.dart'; diff --git a/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart new file mode 100644 index 00000000..8bb5146f --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockStreamChatClient extends Mock implements StreamChatClient {} + +void main() { + test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { + expect(const GalleryFooterThemeData(), + const GalleryFooterThemeData().copyWith()); + expect(const GalleryFooterThemeData().hashCode, + const GalleryFooterThemeData().copyWith().hashCode); + }); + + test( + 'Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, 1), + _galleryFooterThemeDataControlDark); + }); + + test( + 'Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, 0.5), + _galleryFooterThemeDataControlMidLerp); + }); + + test( + 'Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, + _galleryFooterThemeDataControl, 1), + _galleryFooterThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryFooterThemeDataControl + .merge(_galleryFooterThemeDataControlDark), + _galleryFooterThemeDataControlDark); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryFooterThemeDataControlDark + .merge(_galleryFooterThemeDataControl), + _galleryFooterThemeDataControl); + }); + + testWidgets( + 'Passing no GalleryFooterThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryFooter( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageFooterTheme = GalleryFooterTheme.of(_context); + expect(imageFooterTheme.backgroundColor, + _galleryFooterThemeDataControl.backgroundColor); + expect(imageFooterTheme.shareIconColor, + _galleryFooterThemeDataControl.shareIconColor); + expect(imageFooterTheme.titleTextStyle, + _galleryFooterThemeDataControl.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, + _galleryFooterThemeDataControl.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, + _galleryFooterThemeDataControl.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, + _galleryFooterThemeDataControl.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, + _galleryFooterThemeDataControl.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, + _galleryFooterThemeDataControl.bottomSheetPhotosTextStyle); + }); + + testWidgets( + 'Passing no GalleryFooterThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryFooter( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageFooterTheme = GalleryFooterTheme.of(_context); + expect(imageFooterTheme.backgroundColor, + _galleryFooterThemeDataControlDark.backgroundColor); + expect(imageFooterTheme.shareIconColor, + _galleryFooterThemeDataControlDark.shareIconColor); + expect(imageFooterTheme.titleTextStyle, + _galleryFooterThemeDataControlDark.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, + _galleryFooterThemeDataControlDark.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, + _galleryFooterThemeDataControlDark.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, + _galleryFooterThemeDataControlDark.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, + _galleryFooterThemeDataControlDark.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, + _galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle); + }); +} + +// Light theme control +final _galleryFooterThemeDataControl = GalleryFooterThemeData( + backgroundColor: ColorTheme.light().barsBg, + shareIconColor: ColorTheme.light().textHighEmphasis, + titleTextStyle: TextTheme.light().headlineBold, + gridIconButtonColor: ColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: ColorTheme.light().barsBg, + bottomSheetBarrierColor: ColorTheme.light().overlay, + bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: TextTheme.light().headlineBold, +); + +// Mid-lerp theme control +const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( + backgroundColor: Color(0xff87898b), + shareIconColor: Color(0xff7f7f7f), + titleTextStyle: TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + gridIconButtonColor: Color(0xff7f7f7f), + bottomSheetBarrierColor: Color(0x4c000000), + bottomSheetBackgroundColor: Color(0xff87898b), + bottomSheetPhotosTextStyle: TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + bottomSheetCloseIconColor: Color(0xff7f7f7f), +); + +// Dark theme control +final _galleryFooterThemeDataControlDark = GalleryFooterThemeData( + backgroundColor: ColorTheme.dark().barsBg, + shareIconColor: ColorTheme.dark().textHighEmphasis, + titleTextStyle: TextTheme.dark().headlineBold, + gridIconButtonColor: ColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: ColorTheme.dark().barsBg, + bottomSheetBarrierColor: ColorTheme.dark().overlay, + bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold, +); diff --git a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart new file mode 100644 index 00000000..9dca4ce5 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockStreamChatClient extends Mock implements StreamChatClient {} + +void main() { + test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { + expect(const GalleryHeaderThemeData(), + const GalleryHeaderThemeData().copyWith()); + expect(const GalleryHeaderThemeData().hashCode, + const GalleryHeaderThemeData().copyWith().hashCode); + }); + + test( + 'Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, 1), + _galleryHeaderThemeDataDarkControl); + }); + + test( + 'Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, 0.5), + _galleryHeaderThemeDataHalfLerpControl); + }); + + test( + 'Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, + _galleryHeaderThemeDataControl, 1), + _galleryHeaderThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryHeaderThemeDataControl + .merge(_galleryHeaderThemeDataDarkControl), + _galleryHeaderThemeDataDarkControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryHeaderThemeDataDarkControl + .merge(_galleryHeaderThemeDataControl), + _galleryHeaderThemeDataControl); + }); + + testWidgets( + 'Passing no GalleryHeaderThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryHeader( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageHeaderTheme = GalleryHeaderTheme.of(_context); + expect(imageHeaderTheme.closeButtonColor, + _galleryHeaderThemeDataControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, + _galleryHeaderThemeDataControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, + _galleryHeaderThemeDataControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, + _galleryHeaderThemeDataControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, + _galleryHeaderThemeDataControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, + _galleryHeaderThemeDataControl.bottomSheetBarrierColor); + }); + + testWidgets( + 'Passing no GalleryHeaderThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryHeader( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageHeaderTheme = GalleryHeaderTheme.of(_context); + expect(imageHeaderTheme.closeButtonColor, + _galleryHeaderThemeDataDarkControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, + _galleryHeaderThemeDataDarkControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, + _galleryHeaderThemeDataDarkControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, + _galleryHeaderThemeDataDarkControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, + _galleryHeaderThemeDataDarkControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, + _galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor); + }); +} + +// Light theme test control. +final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xff000000), + backgroundColor: const Color(0xffffffff), + iconMenuPointColor: const Color(0xff000000), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Colors.black, + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2), +); + +// Light theme test control. +final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xff7f7f7f), + backgroundColor: const Color(0xff87898b), + iconMenuPointColor: const Color(0xff7f7f7f), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xff7f7f7f), + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Color(0xff7a7a7a), + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color(0x4c000000), +); + +// Dark theme test control. +final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xffffffff), + backgroundColor: const Color(0xff101418), + iconMenuPointColor: const Color(0xffffffff), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Colors.white, + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4), +); diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index e2809d11..0fe3349a 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -37,7 +37,7 @@ void main() { child: WillPopScope( onWillPop: () async => false, child: Scaffold( - body: ImageFooter( + body: GalleryFooter( message: Message(), ), ),