From 68db4bfee4f3cf7611c34fd3fe7826651b84b3b6 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 16 Jul 2021 12:15:04 -0400 Subject: [PATCH] chore: Allow MessageListView background color to be customized via theme This commit adds the MessageListViewTheme and MessageListViewTheme data classes. The customization is enabled in message_list_view.dart via the addition of a ColoredBox widget that looks up the widget tree for the background color set by MessageListViewThemeData --- .../lib/src/message_list_view.dart | 383 +++++++++--------- .../lib/src/stream_chat_theme.dart | 122 +++++- 2 files changed, 314 insertions(+), 191 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 822a2f5a..fee3c8ee 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -386,203 +386,206 @@ class _MessageListViewState extends State { 1 // parent message ; - return Stack( - alignment: Alignment.center, - children: [ - ConnectionStatusBuilder( - statusBuilder: (context, status) { - var statusString = ''; - var showStatus = true; - switch (status) { - case ConnectionStatus.connected: - statusString = 'Connected'; - showStatus = false; - break; - case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; - break; - case ConnectionStatus.disconnected: - statusString = 'Disconnected'; - break; - } + return ColoredBox( + color: MessageListViewTheme.of(context).backgroundColor!, + child: Stack( + alignment: Alignment.center, + children: [ + ConnectionStatusBuilder( + statusBuilder: (context, status) { + var statusString = ''; + var showStatus = true; + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } - return InfoTile( - showMessage: widget.showConnectionStateTile && showStatus, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: statusString, - child: LazyLoadScrollView( - onPageScrollStart: () { - FocusScope.of(context).unfocus(); - }, - onStartOfPage: () async { - _inBetweenList = false; - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData( - streamChannel, - QueryDirection.bottom, - ); - } - }, - onEndOfPage: () async { - _inBetweenList = false; - _topPaginationActive = true; - _bottomPaginationActive = false; - return _paginateData( - streamChannel, - QueryDirection.top, - ); - }, - onInBetweenOfPage: () { - _inBetweenList = true; - }, - child: ScrollablePositionedList.separated( - key: ValueKey(initialIndex! + initialAlignment!), - itemPositionsListener: _itemPositionListener, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - addAutomaticKeepAlives: false, - itemCount: itemCount, - - // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) - // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| - // ParentMessage -> 7 (count-1) - // Separator(ThreadSeparator) -> 6 (count-2) - // Header -> 6 (count-2) - // Separator(Header -> 8??T -> 0||52) -> 5 (count-3) - // TopLoader -> 5 (count-3) - // Separator(0) -> 4 (count-4) - // Message -> 4 (count-4) - // Separator(2||8) -> 3 (count-5) - // Message -> 3 (count-5) - // Separator(2||8) -> 2 (count-6) - // Message -> 2 (count-6) - // Separator(0) -> 1 (count-7) - // BottomLoader -> 1 (count-7) - // Separator(Footer -> 8??30) -> 0 (count-8) - // Footer -> 0 (count-8) - - separatorBuilder: (context, i) { - if (i == itemCount - 2) { - if (widget.parentMessage == null) { - return const Offstage(); - } - return _buildThreadSeparator(); - } - if (i == itemCount - 3) { - if (widget.headerBuilder == null) { - if (_isThreadConversation) return const Offstage(); - return const SizedBox(height: 52); - } - return const SizedBox(height: 8); - } - if (i == 0) { - if (widget.footerBuilder == null) { - return const SizedBox(height: 30); - } - return const SizedBox(height: 8); - } - - if (i == 1 || i == itemCount - 4) return const Offstage(); - - final message = messages[i - 1]; - final nextMessage = messages[i - 2]; - if (!Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), - Units.DAY, - )) { - final divider = widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!( - nextMessage.createdAt.toLocal(), - ) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: divider, - ); - } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, - ); - - final isNextUserSame = - message.user!.id == nextMessage.user?.id; - final isThread = message.replyCount! > 0; - final isDeleted = message.isDeleted; - if (timeDiff >= 1 || - !isNextUserSame || - isThread || - isDeleted) { - return const SizedBox(height: 8); - } - return const SizedBox(height: 2); + return InfoTile( + showMessage: widget.showConnectionStateTile && showStatus, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: LazyLoadScrollView( + onPageScrollStart: () { + FocusScope.of(context).unfocus(); }, - itemBuilder: (context, i) { - if (i == itemCount - 1) { - if (widget.parentMessage == null) return const Offstage(); - return buildParentMessage(widget.parentMessage!); - } - - if (i == itemCount - 2) { - return widget.headerBuilder?.call(context) ?? - const Offstage(); - } - - if (i == itemCount - 3) { - return _buildLoadingIndicator( - streamChannel!, - QueryDirection.top, - ); - } - - if (i == 1) { - return _buildLoadingIndicator( - streamChannel!, + onStartOfPage: () async { + _inBetweenList = false; + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + return _paginateData( + streamChannel, QueryDirection.bottom, ); } - - if (i == 0) { - return widget.footerBuilder?.call(context) ?? - const Offstage(); - } - - const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer - - final message = messages[i - 2]; - Widget messageWidget; - - if (i == bottomMessageIndex) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel!, - i - 2, - ); - } else { - messageWidget = buildMessage(message, messages, i - 2); - } - return messageWidget; }, + onEndOfPage: () async { + _inBetweenList = false; + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; + }, + child: ScrollablePositionedList.separated( + key: ValueKey(initialIndex! + initialAlignment!), + itemPositionsListener: _itemPositionListener, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + addAutomaticKeepAlives: false, + itemCount: itemCount, + + // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) + // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| + // ParentMessage -> 7 (count-1) + // Separator(ThreadSeparator) -> 6 (count-2) + // Header -> 6 (count-2) + // Separator(Header -> 8??T -> 0||52) -> 5 (count-3) + // TopLoader -> 5 (count-3) + // Separator(0) -> 4 (count-4) + // Message -> 4 (count-4) + // Separator(2||8) -> 3 (count-5) + // Message -> 3 (count-5) + // Separator(2||8) -> 2 (count-6) + // Message -> 2 (count-6) + // Separator(0) -> 1 (count-7) + // BottomLoader -> 1 (count-7) + // Separator(Footer -> 8??30) -> 0 (count-8) + // Footer -> 0 (count-8) + + separatorBuilder: (context, i) { + if (i == itemCount - 2) { + if (widget.parentMessage == null) { + return const Offstage(); + } + return _buildThreadSeparator(); + } + if (i == itemCount - 3) { + if (widget.headerBuilder == null) { + if (_isThreadConversation) return const Offstage(); + return const SizedBox(height: 52); + } + return const SizedBox(height: 8); + } + if (i == 0) { + if (widget.footerBuilder == null) { + return const SizedBox(height: 30); + } + return const SizedBox(height: 8); + } + + if (i == 1 || i == itemCount - 4) return const Offstage(); + + final message = messages[i - 1]; + final nextMessage = messages[i - 2]; + if (!Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), + Units.DAY, + )) { + final divider = widget.dateDividerBuilder != null + ? widget.dateDividerBuilder!( + nextMessage.createdAt.toLocal(), + ) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: divider, + ); + } + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + + final isNextUserSame = + message.user!.id == nextMessage.user?.id; + final isThread = message.replyCount! > 0; + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { + return const SizedBox(height: 8); + } + return const SizedBox(height: 2); + }, + itemBuilder: (context, i) { + if (i == itemCount - 1) { + if (widget.parentMessage == null) return const Offstage(); + return buildParentMessage(widget.parentMessage!); + } + + if (i == itemCount - 2) { + return widget.headerBuilder?.call(context) ?? + const Offstage(); + } + + if (i == itemCount - 3) { + return _buildLoadingIndicator( + streamChannel!, + QueryDirection.top, + ); + } + + if (i == 1) { + return _buildLoadingIndicator( + streamChannel!, + QueryDirection.bottom, + ); + } + + if (i == 0) { + return widget.footerBuilder?.call(context) ?? + const Offstage(); + } + + const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer + + final message = messages[i - 2]; + Widget messageWidget; + + if (i == bottomMessageIndex) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel!, + i - 2, + ); + } else { + messageWidget = buildMessage(message, messages, i - 2); + } + return messageWidget; + }, + ), ), - ), - ); - }, - ), - if (widget.showScrollToBottom) _buildScrollToBottom(), - if (widget.showFloatingDateDivider) - _buildFloatingDateDivider(itemCount), - ], + ); + }, + ), + if (widget.showScrollToBottom) _buildScrollToBottom(), + if (widget.showFloatingDateDivider) + _buildFloatingDateDivider(itemCount), + ], + ), ); } 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 c62e92ec..6bf722dc 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -60,6 +60,7 @@ class StreamChatThemeData { List? reactionIcons, GalleryHeaderThemeData? imageHeaderTheme, GalleryFooterThemeData? imageFooterTheme, + MessageListViewThemeData? messageListViewTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -83,6 +84,7 @@ class StreamChatThemeData { reactionIcons: reactionIcons, galleryHeaderTheme: imageHeaderTheme, galleryFooterTheme: imageFooterTheme, + messageListViewTheme: messageListViewTheme, ); return defaultData.merge(customizedData); @@ -111,6 +113,7 @@ class StreamChatThemeData { required this.reactionIcons, required this.galleryHeaderTheme, required this.galleryFooterTheme, + required this.messageListViewTheme, }); /// Create a theme from a Material [Theme] @@ -166,6 +169,9 @@ class StreamChatThemeData { /// Assets used for rendering reactions final List reactionIcons; + /// + final MessageListViewThemeData messageListViewTheme; + /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -182,6 +188,7 @@ class StreamChatThemeData { List? reactionIcons, GalleryHeaderThemeData? galleryHeaderTheme, GalleryFooterThemeData? galleryFooterTheme, + MessageListViewThemeData? messageListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -199,6 +206,7 @@ class StreamChatThemeData { reactionIcons: reactionIcons ?? this.reactionIcons, galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, + messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, ); /// Merge themes @@ -219,6 +227,8 @@ class StreamChatThemeData { reactionIcons: other.reactionIcons, galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), + messageListViewTheme: + messageListViewTheme.merge(other.messageListViewTheme), ); } @@ -438,6 +448,9 @@ class StreamChatThemeData { bottomSheetPhotosTextStyle: textTheme.headlineBold, bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), + messageListViewTheme: MessageListViewThemeData( + backgroundColor: colorTheme.barsBg, + ), ); } } @@ -1651,7 +1664,7 @@ class GalleryFooterThemeData with Diagnosticable { a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), ); - /// Merges one [GalleryFooterThemeData] with the another + /// Merges one [GalleryFooterThemeData] with another. GalleryFooterThemeData merge(GalleryFooterThemeData? other) { if (other == null) return this; return copyWith( @@ -1708,3 +1721,110 @@ class GalleryFooterThemeData with Diagnosticable { 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); } } + +/// Overrides the default style of [MessageListView] descendants. +/// +/// See also: +/// +/// * [MessageListViewThemeData], which is used to configure this theme. +class MessageListViewTheme extends InheritedTheme { + /// Creates a [MessageListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageListViewTheme] widget, then + /// [StreamChatThemeData.messageListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageListViewTheme theme = ImageFooterTheme.of(context); + /// ``` + static MessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return messageListViewTheme?.data ?? + StreamChatTheme.of(context).messageListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageListView]s when +/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageListViewTheme]. +/// +/// See also: +/// +/// * [MessageListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.messageListViewTheme], which can be used to override +/// the default style for [MessageListView]s below the overall +/// [StreamChatTheme]. +class MessageListViewThemeData with Diagnosticable { + /// Creates a [MessageListViewThemeData]. + const MessageListViewThemeData({ + required this.backgroundColor, + }); + + /// The color of the [MessageListView] background. + final Color? backgroundColor; + + /// Copies this [MessageListViewThemeData] to another. + MessageListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [MessageListView] themes. + /// + /// All the properties must be non-null. + MessageListViewThemeData lerp( + MessageListViewThemeData a, + MessageListViewThemeData b, + double t, + ) => + MessageListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, a.backgroundColor, t)); + + /// Merges one [MessageListViewThemeData] with another. + MessageListViewThemeData merge(MessageListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +}