Merge pull request #713 from esarbanis/background-image

feat(ui): add the ability to add a background image in MessageListView
This commit is contained in:
Salvatore Giordano
2021-10-11 13:57:03 +02:00
committed by GitHub
10 changed files with 160 additions and 11 deletions
@@ -2,6 +2,10 @@
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
✅ Added
- `MessageListViewThemeData` now accepts a `DecorationImage` as a background image for `MessageListView`.
## 3.1.1
- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -63,6 +63,12 @@ class MyApp extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
),
),
messageListViewTheme: const MessageListViewThemeData(
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
)),
otherMessageTheme: MessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle(
@@ -47,8 +47,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
assets:
- assets/background_doodle.png
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
@@ -638,10 +638,14 @@ class _MessageListViewState extends State<MessageListView> {
);
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
final backgroundImage = MessageListViewTheme.of(context).backgroundImage;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
if (backgroundColor != null || backgroundImage != null) {
return Container(
decoration: BoxDecoration(
color: backgroundColor,
image: backgroundImage,
),
child: child,
);
}
@@ -965,7 +969,7 @@ class _MessageListViewState extends State<MessageListView> {
}
final userId = StreamChat.of(context).currentUser!.id;
final isMyMessage = message.user!.id == userId;
final isMyMessage = message.user?.id == userId;
final nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
final isNextUserSame =
nextMessage != null && message.user!.id == nextMessage.user!.id;
@@ -987,7 +987,7 @@ class _MessageWidgetState extends State<MessageWidget>
return widget.usernameBuilder!(context, widget.message);
}
return Text(
widget.message.user!.name,
widget.message.user?.name ?? '',
maxLines: 1,
key: usernameKey,
style: widget.messageTheme.messageAuthorStyle,
@@ -60,17 +60,23 @@ class MessageListViewThemeData with Diagnosticable {
/// Creates a [MessageListViewThemeData].
const MessageListViewThemeData({
this.backgroundColor,
this.backgroundImage,
});
/// The color of the [MessageListView] background.
final Color? backgroundColor;
/// The image of the [MessageListView] background.
final DecorationImage? backgroundImage;
/// Copies this [MessageListViewThemeData] to another.
MessageListViewThemeData copyWith({
Color? backgroundColor,
DecorationImage? backgroundImage,
}) =>
MessageListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
backgroundImage: backgroundImage ?? this.backgroundImage,
);
/// Linearly interpolate between two [MessageListView] themes.
@@ -83,6 +89,7 @@ class MessageListViewThemeData with Diagnosticable {
) =>
MessageListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage,
);
/// Merges one [MessageListViewThemeData] with another.
@@ -90,6 +97,7 @@ class MessageListViewThemeData with Diagnosticable {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
backgroundImage: other.backgroundImage,
);
}
@@ -98,14 +106,20 @@ class MessageListViewThemeData with Diagnosticable {
identical(this, other) ||
other is MessageListViewThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor;
backgroundColor == other.backgroundColor &&
backgroundImage == other.backgroundImage;
@override
int get hashCode => backgroundColor.hashCode;
int get hashCode => backgroundColor.hashCode + backgroundImage.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('backgroundColor', backgroundColor));
properties
..add(ColorProperty('backgroundColor', backgroundColor))
..add(
DiagnosticsProperty<DecorationImage>('backgroundImage', backgroundImage,
defaultValue: null),
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -8,13 +9,21 @@ import 'mocks.dart';
void main() {
late StreamChatClient client;
late Channel channel;
late ChannelClientState channelClientState;
late ClientState clientState;
setUp(() {
client = MockClient();
clientState = MockClientState();
when(() => client.state).thenAnswer((_) => clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'testid'));
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(OwnUser(id: 'testid')));
channel = MockChannel();
when(() => channel.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
final channelClientState = MockChannelState();
channelClientState = MockChannelState();
when(() => channel.client).thenReturn(client);
when(() => channel.state).thenReturn(channelClientState);
when(() => channelClientState.threadsStream)
.thenAnswer((_) => const Stream.empty());
@@ -22,6 +31,17 @@ void main() {
.thenAnswer((_) => const Stream.empty());
when(() => channelClientState.messages).thenReturn([]);
when(() => channelClientState.isUpToDate).thenReturn(true);
when(() => channelClientState.isUpToDateStream)
.thenAnswer((_) => Stream.value(true));
when(() => channelClientState.unreadCountStream)
.thenAnswer((_) => Stream.value(0));
when(() => channelClientState.unreadCount).thenReturn(0);
when(() => channelClientState.readStream)
.thenAnswer((_) => const Stream.empty());
when(() => channelClientState.read).thenReturn([]);
when(() => channelClientState.membersStream)
.thenAnswer((_) => const Stream.empty());
when(() => channelClientState.members).thenReturn([]);
});
// https://github.com/GetStream/stream-chat-flutter/issues/674
@@ -46,4 +66,65 @@ void main() {
expect(find.byType(MessageListView), findsOneWidget);
expect(find.byKey(emptyWidgetKey), findsOneWidget);
});
testWidgets('renders a non empty message list view with custom background',
(tester) async {
final message = Message(
id: 'message1',
text: 'Hello world!',
user: User(
id: 'user',
name: 'Test User',
),
);
when(() => channelClientState.messagesStream).thenAnswer(
(_) => Stream.value([message]),
);
when(() => channelClientState.messages).thenReturn([message]);
const nonEmptyWidgetKey = Key('non_empty_widget');
await tester.runAsync(() async {
await tester.pumpWidget(
MaterialApp(
home: DefaultAssetBundle(
bundle: rootBundle,
child: StreamChat(
client: client,
streamChatThemeData: StreamChatThemeData.light().copyWith(
messageListViewTheme: const MessageListViewThemeData(
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('images/placeholder.png'),
fit: BoxFit.none,
),
),
),
child: StreamChannel(
channel: channel,
child: const MessageListView(
key: nonEmptyWidgetKey,
),
),
),
),
),
);
await tester.pumpAndSettle();
});
bool findBackground(Widget widget) =>
widget is Container &&
widget.decoration is BoxDecoration &&
(widget.decoration! as BoxDecoration).image != null;
expect(find.byType(MessageListView), findsOneWidget);
expect(find.byKey(nonEmptyWidgetKey), findsOneWidget);
expect(
find.byWidgetPredicate(
findBackground,
description: 'findBackground',
),
findsOneWidget);
});
}
@@ -5,6 +5,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
class MockClient extends Mock implements StreamChatClient {
MockClient() {
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
when(() => wsConnectionStatusStream)
.thenAnswer((_) => Stream.value(ConnectionStatus.connected));
}
}
@@ -109,6 +109,37 @@ void main() {
expect(messageListViewTheme.backgroundColor,
_messageListViewThemeDataControlDark.backgroundColor);
});
testWidgets(
'Pass backgroundImage to MessageListViewThemeData return backgroundImage',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
streamChatThemeData: StreamChatThemeData.light()
.copyWith(messageListViewTheme: _messageListViewThemeDataImage),
child: child,
),
home: Builder(
builder: (BuildContext context) {
_context = context;
return Scaffold(
body: StreamChannel(
channel: MockChannel(),
child: const MessageListView(),
),
);
},
),
),
);
final messageListViewTheme = MessageListViewTheme.of(_context);
expect(messageListViewTheme.backgroundImage,
_messageListViewThemeDataImage.backgroundImage);
});
}
final _messageListViewThemeDataControl = MessageListViewThemeData(
@@ -122,3 +153,10 @@ const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData(
final _messageListViewThemeDataControlDark = MessageListViewThemeData(
backgroundColor: ColorTheme.dark().barsBg,
);
const _messageListViewThemeDataImage = MessageListViewThemeData(
backgroundImage: DecorationImage(
image: AssetImage('example/assets/background_doodle.png'),
fit: BoxFit.cover,
),
);