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:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
|
- 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
|
## 3.1.1
|
||||||
|
|
||||||
- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
- 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),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
messageListViewTheme: const MessageListViewThemeData(
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
backgroundImage: DecorationImage(
|
||||||
|
image: AssetImage('assets/background_doodle.png'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)),
|
||||||
otherMessageTheme: MessageThemeData(
|
otherMessageTheme: MessageThemeData(
|
||||||
messageBackgroundColor: colorTheme.textHighEmphasis,
|
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||||
messageTextStyle: TextStyle(
|
messageTextStyle: TextStyle(
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/background_doodle.png
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# 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 backgroundColor = MessageListViewTheme.of(context).backgroundColor;
|
||||||
|
final backgroundImage = MessageListViewTheme.of(context).backgroundImage;
|
||||||
|
|
||||||
if (backgroundColor != null) {
|
if (backgroundColor != null || backgroundImage != null) {
|
||||||
return ColoredBox(
|
return Container(
|
||||||
color: backgroundColor,
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
image: backgroundImage,
|
||||||
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -965,7 +969,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final userId = StreamChat.of(context).currentUser!.id;
|
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 nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
|
||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
nextMessage != null && message.user!.id == nextMessage.user!.id;
|
nextMessage != null && message.user!.id == nextMessage.user!.id;
|
||||||
|
|||||||
@@ -987,7 +987,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
return widget.usernameBuilder!(context, widget.message);
|
return widget.usernameBuilder!(context, widget.message);
|
||||||
}
|
}
|
||||||
return Text(
|
return Text(
|
||||||
widget.message.user!.name,
|
widget.message.user?.name ?? '',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
key: usernameKey,
|
key: usernameKey,
|
||||||
style: widget.messageTheme.messageAuthorStyle,
|
style: widget.messageTheme.messageAuthorStyle,
|
||||||
|
|||||||
@@ -60,17 +60,23 @@ class MessageListViewThemeData with Diagnosticable {
|
|||||||
/// Creates a [MessageListViewThemeData].
|
/// Creates a [MessageListViewThemeData].
|
||||||
const MessageListViewThemeData({
|
const MessageListViewThemeData({
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.backgroundImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The color of the [MessageListView] background.
|
/// The color of the [MessageListView] background.
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// The image of the [MessageListView] background.
|
||||||
|
final DecorationImage? backgroundImage;
|
||||||
|
|
||||||
/// Copies this [MessageListViewThemeData] to another.
|
/// Copies this [MessageListViewThemeData] to another.
|
||||||
MessageListViewThemeData copyWith({
|
MessageListViewThemeData copyWith({
|
||||||
Color? backgroundColor,
|
Color? backgroundColor,
|
||||||
|
DecorationImage? backgroundImage,
|
||||||
}) =>
|
}) =>
|
||||||
MessageListViewThemeData(
|
MessageListViewThemeData(
|
||||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
|
backgroundImage: backgroundImage ?? this.backgroundImage,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Linearly interpolate between two [MessageListView] themes.
|
/// Linearly interpolate between two [MessageListView] themes.
|
||||||
@@ -83,6 +89,7 @@ class MessageListViewThemeData with Diagnosticable {
|
|||||||
) =>
|
) =>
|
||||||
MessageListViewThemeData(
|
MessageListViewThemeData(
|
||||||
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
||||||
|
backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Merges one [MessageListViewThemeData] with another.
|
/// Merges one [MessageListViewThemeData] with another.
|
||||||
@@ -90,6 +97,7 @@ class MessageListViewThemeData with Diagnosticable {
|
|||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
backgroundColor: other.backgroundColor,
|
backgroundColor: other.backgroundColor,
|
||||||
|
backgroundImage: other.backgroundImage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,14 +106,20 @@ class MessageListViewThemeData with Diagnosticable {
|
|||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is MessageListViewThemeData &&
|
other is MessageListViewThemeData &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
backgroundColor == other.backgroundColor;
|
backgroundColor == other.backgroundColor &&
|
||||||
|
backgroundImage == other.backgroundImage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => backgroundColor.hashCode;
|
int get hashCode => backgroundColor.hashCode + backgroundImage.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(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/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
@@ -8,13 +9,21 @@ import 'mocks.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
late StreamChatClient client;
|
late StreamChatClient client;
|
||||||
late Channel channel;
|
late Channel channel;
|
||||||
|
late ChannelClientState channelClientState;
|
||||||
|
late ClientState clientState;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
client = MockClient();
|
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();
|
channel = MockChannel();
|
||||||
when(() => channel.on(any(), any(), any(), any()))
|
when(() => channel.on(any(), any(), any(), any()))
|
||||||
.thenAnswer((_) => const Stream.empty());
|
.thenAnswer((_) => const Stream.empty());
|
||||||
final channelClientState = MockChannelState();
|
channelClientState = MockChannelState();
|
||||||
|
when(() => channel.client).thenReturn(client);
|
||||||
when(() => channel.state).thenReturn(channelClientState);
|
when(() => channel.state).thenReturn(channelClientState);
|
||||||
when(() => channelClientState.threadsStream)
|
when(() => channelClientState.threadsStream)
|
||||||
.thenAnswer((_) => const Stream.empty());
|
.thenAnswer((_) => const Stream.empty());
|
||||||
@@ -22,6 +31,17 @@ void main() {
|
|||||||
.thenAnswer((_) => const Stream.empty());
|
.thenAnswer((_) => const Stream.empty());
|
||||||
when(() => channelClientState.messages).thenReturn([]);
|
when(() => channelClientState.messages).thenReturn([]);
|
||||||
when(() => channelClientState.isUpToDate).thenReturn(true);
|
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
|
// https://github.com/GetStream/stream-chat-flutter/issues/674
|
||||||
@@ -46,4 +66,65 @@ void main() {
|
|||||||
expect(find.byType(MessageListView), findsOneWidget);
|
expect(find.byType(MessageListView), findsOneWidget);
|
||||||
expect(find.byKey(emptyWidgetKey), 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 {
|
class MockClient extends Mock implements StreamChatClient {
|
||||||
MockClient() {
|
MockClient() {
|
||||||
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
|
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
|
||||||
|
when(() => wsConnectionStatusStream)
|
||||||
|
.thenAnswer((_) => Stream.value(ConnectionStatus.connected));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,37 @@ void main() {
|
|||||||
expect(messageListViewTheme.backgroundColor,
|
expect(messageListViewTheme.backgroundColor,
|
||||||
_messageListViewThemeDataControlDark.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(
|
final _messageListViewThemeDataControl = MessageListViewThemeData(
|
||||||
@@ -122,3 +153,10 @@ const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData(
|
|||||||
final _messageListViewThemeDataControlDark = MessageListViewThemeData(
|
final _messageListViewThemeDataControlDark = MessageListViewThemeData(
|
||||||
backgroundColor: ColorTheme.dark().barsBg,
|
backgroundColor: ColorTheme.dark().barsBg,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const _messageListViewThemeDataImage = MessageListViewThemeData(
|
||||||
|
backgroundImage: DecorationImage(
|
||||||
|
image: AssetImage('example/assets/background_doodle.png'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user