Merge remote-tracking branch 'origin/develop' into cds-328

# Conflicts:
#	packages/stream_chat/CHANGELOG.md
This commit is contained in:
xsahil03x
2021-07-26 14:15:48 +05:30
38 changed files with 890 additions and 352 deletions
+4 -2
View File
@@ -12,13 +12,15 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v2.1.0
- uses: amannn/action-semantic-pull-request@v3.4.0
with:
scopes: |
llc
persistence
core
ui
doc
repo
requireScope: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -1
View File
@@ -8,6 +8,11 @@
- The `Message` class now has an `i18n` field for translations
- The `User` class now has a `language` field for the user's language preference.
🐞 Fixed
- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working
- [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*`
## 2.0.0
🛑️ Breaking Changes from `1.5.3`
@@ -629,4 +634,4 @@
## 0.0.2
- first beta version
- first beta version
@@ -25,12 +25,14 @@ import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/own_user.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
import 'package:stream_chat/src/core/util/utils.dart';
import 'package:stream_chat/src/db/chat_persistence_client.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/location.dart';
import 'package:stream_chat/src/ws/connection_status.dart';
import 'package:stream_chat/src/ws/websocket.dart';
import 'package:stream_chat/version.dart';
/// Handler function used for logging records. Function requires a single
/// [LogRecord] as the only parameter.
@@ -42,6 +44,10 @@ final _levelEmojiMapper = {
Level.SEVERE: '🚨',
};
final _userAgent = 'stream-chat-dart-client-'
'${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
/// The official Dart client for Stream Chat,
/// a service for building chat applications.
/// This library can be used on any Dart project and on both mobile and web apps
@@ -80,6 +86,7 @@ class StreamChatClient {
location: location,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: {'X-Stream-Client': _userAgent},
);
_chatApi = chatApi ??
@@ -99,6 +106,7 @@ class StreamChatClient {
tokenManager: _tokenManager,
handler: handleEvent,
logger: detachedLogger('🔌'),
queryParameters: {'X-Stream-Client': _userAgent},
);
_retryPolicy = retryPolicy ??
@@ -324,7 +332,7 @@ class StreamChatClient {
} catch (e, stk) {
if (e is StreamWebSocketError && e.isRetriable) {
final event = await _chatPersistenceClient?.getConnectionInfo();
if (event != null) return event.me?.merge(ownUser) ?? ownUser;
if (event != null) return ownUser.merge(event.me);
}
logger.severe('error connecting user : ${ownUser.id}', e, stk);
rethrow;
@@ -363,7 +371,7 @@ class StreamChatClient {
try {
final event = await _ws.connect(user);
return event.me?.merge(user) ?? user;
return user.merge(event.me);
} catch (e, stk) {
logger.severe('error connecting ws', e, stk);
rethrow;
@@ -289,6 +289,7 @@ class ChannelApi {
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/stop-watching',
data: {},
);
return EmptyResponse.fromJson(response.data);
}
@@ -10,9 +10,7 @@ import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
import 'package:stream_chat/src/location.dart';
import 'package:stream_chat/version.dart';
part 'stream_http_client_options.dart';
@@ -33,11 +31,14 @@ class StreamHttpClient {
..options.baseUrl = _options.baseUrl
..options.receiveTimeout = _options.receiveTimeout.inMilliseconds
..options.connectTimeout = _options.connectTimeout.inMilliseconds
..options.queryParameters = {'api_key': apiKey}
..options.queryParameters = {
'api_key': apiKey,
..._options.queryParameters,
}
..options.headers = {
'Content-Type': 'application/json',
'X-Stream-Client': _options.userAgent,
'Content-Encoding': 'application/gzip',
..._options.headers,
}
..interceptors.addAll([
if (tokenManager != null) AuthInterceptor(this, tokenManager),
@@ -10,6 +10,8 @@ class StreamHttpClientOptions {
this.location,
this.connectTimeout = const Duration(seconds: 6),
this.receiveTimeout = const Duration(seconds: 6),
this.queryParameters = const {},
this.headers = const {},
}) : _baseUrl = baseUrl ?? _defaultBaseURL;
final String _baseUrl;
@@ -32,8 +34,20 @@ class StreamHttpClientOptions {
/// received timeout, default to 6s
final Duration receiveTimeout;
/// Get the current user agent
String get userAgent => 'stream-chat-dart-client-'
'${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
/// Common query parameters.
///
/// List values use the default [ListFormat.multiCompatible].
///
/// The value can be overridden per parameter by adding a [MultiParam]
/// object wrapping the actual List value and the desired format.
final Map<String, Object?> queryParameters;
/// Http request headers.
/// The keys of initial headers will be converted to lowercase,
/// for example 'Content-Type' will be converted to 'content-type'.
///
/// The key of Header Map is case-insensitive
/// eg: content-type and Content-Type are
/// regard as the same key.
final Map<String, Object?> headers;
}
@@ -41,11 +41,15 @@ class WebSocket with TimerHelper {
this.reconnectionMonitorInterval = 10,
this.healthCheckInterval = 20,
this.reconnectionMonitorTimeout = 40,
this.queryParameters = const {},
}) : _logger = logger;
///
final String apiKey;
/// Additional query parameters to be added to the websocket url
final Map<String, Object?> queryParameters;
/// WS base url
final String baseUrl;
@@ -156,6 +160,7 @@ class WebSocket with TimerHelper {
'api_key': apiKey,
'authorization': token.rawValue,
'stream-auth-type': token.authType.raw,
...queryParameters,
};
final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws';
final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), '');
@@ -595,14 +595,14 @@ void main() {
final path = '${_getChannelUrl(channelId, channelType)}/stop-watching';
when(() => client.post(path)).thenAnswer(
when(() => client.post(path, data: {})).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await channelApi.stopWatching(channelId, channelType);
expect(res, isNotNull);
verify(() => client.post(path)).called(1);
verify(() => client.post(path, data: {})).called(1);
verifyNoMoreInteractions(client);
});
}
@@ -9,6 +9,8 @@ void main() {
expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com');
expect(options.connectTimeout, const Duration(seconds: 6));
expect(options.receiveTimeout, const Duration(seconds: 6));
expect(options.queryParameters, const {});
expect(options.headers, const {});
});
test('should override all the default set params', () {
@@ -16,11 +18,15 @@ void main() {
baseUrl: 'base-url',
connectTimeout: Duration(seconds: 3),
receiveTimeout: Duration(seconds: 3),
headers: {'test': 'test'},
queryParameters: {'123': '123'},
);
expect(options.location, isNull);
expect(options.baseUrl, 'base-url');
expect(options.connectTimeout, const Duration(seconds: 3));
expect(options.receiveTimeout, const Duration(seconds: 3));
expect(options.headers, {'test': 'test'});
expect(options.queryParameters, {'123': '123'});
});
group('should create baseUrl according to provided location', () {
@@ -96,7 +96,7 @@ void main() {
await client.get('path');
} catch (_) {}
verify(() => logger.info(any())).called(16);
verify(() => logger.info(any())).called(greaterThan(0));
});
test('loggingInterceptor should log error', () async {
@@ -108,7 +108,7 @@ void main() {
await client.get('path');
} catch (_) {}
verify(() => logger.severe(any())).called(8);
verify(() => logger.severe(any())).called(greaterThan(0));
});
test('`.lock` should lock the dio client', () async {
+6 -1
View File
@@ -1,9 +1,14 @@
## Upcoming
Added
✅ Added
- Added `MessageListView.paginationLimit`
- Allow the various ListView widgets to be themed via ThemeData classes
🐞 Fixed
- Fix floating date divider not having a fixed size
## 2.0.0
🛑️ Breaking Changes from `1.5.4`
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 29
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -41,7 +41,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.5.20'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.2'
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
@@ -231,13 +231,21 @@ class _ChannelListViewState extends State<ChannelListView> {
);
}
return ColoredBox(
color: ChannelListViewTheme.of(context).backgroundColor!,
child: LazyLoadScrollView(
onEndOfPage: () => _channelListController.paginateData!(),
child: child,
),
child = LazyLoadScrollView(
onEndOfPage: () => _channelListController.paginateData!(),
child: child,
);
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
}
Widget _buildListView(BuildContext context, List<Channel> channels) {
@@ -0,0 +1,258 @@
import 'dart:math';
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
/// Fallback user avatar with a polygon gradient overlayed with text
class GradientAvatar extends StatefulWidget {
/// Constructor for [GradientAvatar]
const GradientAvatar({
Key? key,
required this.name,
required this.userId,
}) : super(key: key);
/// Name of user to shorten and display
final String name;
/// ID of user to be used for key
final String userId;
@override
_GradientAvatarState createState() => _GradientAvatarState();
}
class _GradientAvatarState extends State<GradientAvatar> {
@override
Widget build(BuildContext context) => Center(
child: RepaintBoundary(
child: CustomPaint(
painter: DemoPainter(
widget.userId,
getShortenedName(widget.name),
DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto',
),
child: const SizedBox.expand(),
),
),
);
String getShortenedName(String name) {
var parts = name.split(' ')..removeWhere((e) => e == '');
if (parts.length > 2) {
parts = parts.take(2).toList();
}
var result = '';
for (var i = 0; i < parts.length; i++) {
result = result + parts[i][0].toUpperCase();
}
return result;
}
}
/// Painter for bg polygon gradient
class DemoPainter extends CustomPainter {
/// Constructor for [DemoPainter]
DemoPainter(
this.userId,
this.username,
this.fontFamily,
);
/// Init grid row count
static const int rowCount = 5;
/// Init grid column count
static const int columnCount = 5;
/// User ID used for key
String userId;
/// User name to display
String username;
/// Font family to use
String fontFamily;
@override
void paint(Canvas canvas, Size size) {
final rowUnit = size.width / columnCount;
final columnUnit = size.height / rowCount;
final rand = Random(userId.length);
final squares = <Offset4>[];
final points = <Offset>{};
final gradient = colorGradients[rand.nextInt(colorGradients.length)];
for (var i = 0; i < rowCount; i++) {
for (var j = 0; j < columnCount; j++) {
final off1 = Offset(rowUnit * j, columnUnit * i);
final off2 = Offset(rowUnit * (j + 1), columnUnit * i);
final off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1));
final off4 = Offset(rowUnit * j, columnUnit * (i + 1));
points.addAll([off1, off2, off3, off4]);
final pointsList = points.toList();
final p1 = pointsList.indexOf(off1);
final p2 = pointsList.indexOf(off2);
final p3 = pointsList.indexOf(off3);
final p4 = pointsList.indexOf(off4);
squares.add(
Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient));
}
}
final list = transformPoints(points, size);
squares.forEach((e) => e.draw(canvas, list));
final smallerSide = size.width > size.height ? size.width : size.height;
final textSize = smallerSide / 3;
final dxShift = (username.length == 2 ? 1.45 : 0.9) * textSize / 2;
final dyShift = (username.length == 2 ? 1.0 : 1.65) * textSize / 2;
final fontSize = username.length == 2 ? textSize : textSize * 1.5;
TextPainter(
text: TextSpan(
text: username,
style: TextStyle(
fontFamily: fontFamily,
fontSize: fontSize,
fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.7),
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr)
..layout(maxWidth: size.width)
..paint(
canvas,
Offset(
(size.width / 2) - dxShift,
(size.height / 2) - dyShift,
),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
/// Transforms initial grid into a polygon grid
List<Offset> transformPoints(Set<Offset> points, Size size) {
final transformedList = <Offset>[];
final orgList = points.toList();
final rand = Random(userId.length);
for (var i = 0; i < points.length; i++) {
final orgDx = orgList[i].dx;
final orgDy = orgList[i].dy;
if (orgDx == 0 ||
orgDy == 0 ||
orgDx == size.width ||
orgDy == size.height) {
transformedList.add(Offset(orgDx, orgDy));
continue;
}
final sign1 = rand.nextInt(2) == 1 ? 1 : -1;
final sign2 = rand.nextInt(2) == 1 ? 1 : -1;
final dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount);
final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount);
transformedList.add(Offset(orgDx + dx, orgDy + dy));
}
return transformedList;
}
}
/// Class for storing and drawing four points of a polygon
class Offset4 {
/// Constructor for [Offset4]
Offset4(
this.p1,
this.p2,
this.p3,
this.p4,
this.row,
this.column,
this.rowSize,
this.colSize,
this.gradient,
);
/// Point 1
int p1;
/// Point 2
int p2;
/// Point 3
int p3;
/// Point 4
int p4;
/// Position of polygon on grid
int row;
/// Position of polygon on grid
int column;
/// Max row size
int rowSize;
/// Max col size
int colSize;
/// Gradient to be applied to polygon
List<Color> gradient;
/// Draw the polygon on canvas
void draw(Canvas canvas, List<Offset> points) {
final paint = Paint()
..color = Color.fromARGB(255, Random().nextInt(255),
Random().nextInt(255), Random().nextInt(255))
..shader = ui.Gradient.linear(
points[p1],
points[p3],
gradient,
);
final backgroundPath = Path()
..moveTo(points[p1].dx, points[p1].dy)
..lineTo(points[p2].dx, points[p2].dy)
..lineTo(points[p3].dx, points[p3].dy)
..lineTo(points[p4].dx, points[p4].dy)
..lineTo(points[p1].dx, points[p1].dy)
..close();
canvas.drawPath(backgroundPath, paint);
}
}
/// Gradient list for polygons
const colorGradients = [
[Color(0xffffafbd), Color(0xffffc3a0)],
[Color(0xff2193b0), Color(0xff6dd5ed)],
[Color(0xffcc2b5e), Color(0xff753a88)],
[Color(0xffee9ca7), Color(0xffffdde1)],
[Color(0xff42275a), Color(0xff734b6d)],
[Color(0xffde6262), Color(0xffffb88c)],
[Color(0xff56ab2f), Color(0xffa8e063)],
[Color(0xff614385), Color(0xff516395)],
[Color(0xffeacda3), Color(0xffd6ae7b)],
[Color(0xff02aab0), Color(0xff00cdac)],
];
@@ -166,11 +166,23 @@ class MessageListView extends StatefulWidget {
this.showFloatingDateDivider = true,
this.threadSeparatorBuilder,
this.messageListController,
this.reverse = true,
this.paginationLimit = 20,
}) : super(key: key);
/// Function used to build a custom message widget
final MessageBuilder? messageBuilder;
/// Whether the view scrolls in the reading direction.
///
/// Defaults to true.
///
/// See [ScrollView.reverse].
final bool reverse;
/// Limit used during pagination
final int paginationLimit;
/// Function used to build a custom system message widget
final SystemMessageBuilder? systemMessageBuilder;
@@ -328,6 +340,7 @@ class _MessageListViewState extends State<MessageListView> {
@override
Widget build(BuildContext context) => MessageListCore(
paginationLimit: widget.paginationLimit,
messageFilter: widget.messageFilter,
loadingBuilder: widget.loadingBuilder ??
(context) => const Center(
@@ -386,210 +399,217 @@ class _MessageListViewState extends State<MessageListView> {
1 // parent message
;
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;
}
final 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();
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: widget.reverse,
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);
},
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
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,
);
}
},
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)
if (i == 0) {
return widget.footerBuilder?.call(context) ??
const Offstage();
}
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);
}
const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
if (i == 1 || i == itemCount - 4) return const Offstage();
final message = messages[i - 2];
Widget messageWidget;
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,
if (i == bottomMessageIndex) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel!,
i - 2,
);
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;
},
),
} 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),
],
);
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
}
Widget _buildThreadSeparator() {
@@ -614,7 +634,10 @@ class _MessageListViewState extends State<MessageListView> {
}
Positioned _buildFloatingDateDivider(int itemCount) => Positioned(
top: 20,
top: widget.reverse ? 20 : null,
bottom: widget.reverse ? null : 20,
left: 0,
right: 0,
child: BetterStreamBuilder<Iterable<ItemPosition>>(
initialData: _itemPositionListener.itemPositions.value,
stream: _itemPositionStream,
@@ -646,7 +669,9 @@ class _MessageListViewState extends State<MessageListView> {
);
Future<void> _paginateData(
StreamChannelState? channel, QueryDirection direction) =>
StreamChannelState? channel,
QueryDirection direction,
) =>
_messageListController.paginateData!(direction: direction);
int? _getTopElementIndex(Iterable<ItemPosition> values) {
@@ -706,9 +731,13 @@ class _MessageListViewState extends State<MessageListView> {
);
}
},
child: StreamSvgIcon.down(
color: _streamTheme.colorTheme.textHighEmphasis,
),
child: widget.reverse
? StreamSvgIcon.down(
color: _streamTheme.colorTheme.textHighEmphasis,
)
: StreamSvgIcon.up(
color: _streamTheme.colorTheme.textHighEmphasis,
),
),
if (showUnreadCount)
Positioned(
@@ -144,61 +144,72 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
widget.messageSearchListController ?? _defaultController;
@override
Widget build(BuildContext context) => ColoredBox(
color: MessageSearchListViewTheme.of(context).backgroundColor!,
child: MessageSearchListCore(
filters: widget.filters,
sortOptions: widget.sortOptions,
messageQuery: widget.messageQuery,
paginationParams: widget.paginationParams,
messageFilters: widget.messageFilters,
messageSearchListController: _messageSearchListController,
emptyBuilder: widget.emptyBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: Text('There are no messages currently'),
),
),
Widget build(BuildContext context) {
final messageSearchListCore = MessageSearchListCore(
filters: widget.filters,
sortOptions: widget.sortOptions,
messageQuery: widget.messageQuery,
paginationParams: widget.paginationParams,
messageFilters: widget.messageFilters,
messageSearchListController: _messageSearchListController,
emptyBuilder: widget.emptyBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: Text('There are no messages currently'),
),
),
errorBuilder: widget.errorBuilder ??
(BuildContext context, dynamic error) {
if (error is Error) {
print(error.stackTrace);
}
return InfoTile(
showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: 'An error occurred.',
child: Container(),
);
},
loadingBuilder: widget.loadingBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: CircularProgressIndicator(),
),
),
),
),
errorBuilder: widget.errorBuilder ??
(BuildContext context, dynamic error) {
if (error is Error) {
print(error.stackTrace);
}
return InfoTile(
showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: 'An error occurred.',
child: Container(),
);
},
loadingBuilder: widget.loadingBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: CircularProgressIndicator(),
),
),
childBuilder: widget.childBuilder ?? _buildListView,
),
),
),
childBuilder: widget.childBuilder ?? _buildListView,
);
final backgroundColor =
MessageSearchListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: messageSearchListCore,
);
}
return messageSearchListCore;
}
Widget _separatorBuilder(BuildContext context, int index) => Container(
height: 1,
@@ -1,12 +1,11 @@
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';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/gradient_avatar.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/reaction_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
@@ -310,10 +309,9 @@ class StreamChatThemeData {
colorTheme: colorTheme,
primaryIconTheme: iconTheme,
defaultUserImage: (context, user) => Center(
child: CachedNetworkImage(
filterQuality: FilterQuality.high,
imageUrl: getRandomPicUrl(user),
fit: BoxFit.cover,
child: GradientAvatar(
name: user.name,
userId: user.id,
),
),
channelPreviewTheme: channelPreviewTheme,
@@ -37,6 +37,18 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
/// [StreamSvgIcon] type
factory StreamSvgIcon.up({
double? size,
Color? color,
}) =>
StreamSvgIcon(
assetName: 'Icon_up.svg',
color: color,
width: size,
height: size,
);
/// [StreamSvgIcon] type
factory StreamSvgIcon.attach({
double? size,
@@ -160,38 +160,48 @@ class _UserListViewState extends State<UserListView>
@override
Widget build(BuildContext context) {
final child = ColoredBox(
color: UserListViewTheme.of(context).backgroundColor!,
child: UserListCore(
errorBuilder: widget.errorBuilder ??
(BuildContext context, Object err) => _buildError(err),
emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(),
loadingBuilder: widget.loadingBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: CircularProgressIndicator(),
),
final userListCore = UserListCore(
errorBuilder: widget.errorBuilder ??
(BuildContext context, Object err) => _buildError(err),
emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(),
loadingBuilder: widget.loadingBuilder ??
(context) => LayoutBuilder(
builder: (context, viewportConstraints) =>
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: const Center(
child: CircularProgressIndicator(),
),
),
),
listBuilder:
widget.listBuilder ?? (context, list) => _buildListView(list),
pagination: widget.pagination,
sort: widget.sort,
filter: widget.filter,
presence: widget.presence,
groupAlphabetically: widget.groupAlphabetically,
userListController: _userListController,
),
),
listBuilder:
widget.listBuilder ?? (context, list) => _buildListView(list),
pagination: widget.pagination,
sort: widget.sort,
filter: widget.filter,
presence: widget.presence,
groupAlphabetically: widget.groupAlphabetically,
userListController: _userListController,
);
final backgroundColor = UserListViewTheme.of(context).backgroundColor;
Widget child;
if (backgroundColor != null) {
child = ColoredBox(
color: backgroundColor,
child: userListCore,
);
} else {
child = userListCore;
}
if (!widget.pullToRefresh) {
return child;
} else {
@@ -14,6 +14,7 @@ export 'src/deleted_message.dart';
export 'src/full_screen_media.dart';
export 'src/gallery_footer.dart';
export 'src/gallery_header.dart';
export 'src/gradient_avatar.dart';
export 'src/info_tile.dart';
export 'src/mention_tile.dart';
export 'src/message_action.dart';
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(24, 24) rotate(-180)" d="M5.30605 8.30597C5.11052 8.50222 5.00073 8.76795 5.00073 9.04498C5.00073 9.322 5.11052 9.58774 5.30605 9.78398L11.2061 15.697C11.4321 15.923 11.7351 16.024 12.0291 16C12.17 16.0032 12.3102 15.9778 12.441 15.9252C12.5718 15.8726 12.6906 15.7939 12.7901 15.694L18.6941 9.78398C18.8898 9.58786 18.9998 9.32208 18.9998 9.04498C18.9998 8.76787 18.8898 8.50209 18.6941 8.30597C18.5972 8.20898 18.4821 8.13203 18.3555 8.07952C18.2289 8.02702 18.0931 8 17.9561 8C17.819 8 17.6832 8.02702 17.5566 8.07952C17.43 8.13203 17.3149 8.20898 17.2181 8.30597L11.9981 13.533L6.78005 8.30597C6.68327 8.20901 6.56831 8.13208 6.44176 8.07959C6.31521 8.0271 6.17956 8.00008 6.04255 8.00008C5.90555 8.00008 5.76989 8.0271 5.64334 8.07959C5.51679 8.13208 5.40184 8.20901 5.30505 8.30597H5.30605Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 943 B

+2 -2
View File
@@ -30,8 +30,8 @@ dependencies:
lottie: ^1.0.1
meta: ^1.3.0
path_provider: ^2.0.1
photo_manager: ^1.1.6
photo_view: ^0.11.1
photo_manager: ^1.2.6+1
photo_view: ^0.12.0
rxdart: ^0.27.0
scrollable_positioned_list: ^0.2.0-nullsafety.0
share_plus: ^2.0.3
@@ -1,9 +1,35 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadAppFonts();
goldenFileComparator =
CustomGoldenFileComparator(Uri.parse('test/src/goldens'));
return testMain();
}
class CustomGoldenFileComparator extends LocalFileComparator {
CustomGoldenFileComparator(Uri testFile) : super(testFile);
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final result = await GoldenFileComparator.compareLists(
imageBytes,
await getGoldenBytes(golden),
);
if (!result.passed && result.diffPercent > 0.05) {
final error = await generateFailureOutput(result, golden, basedir);
throw FlutterError(error);
}
return true;
}
@override
Future<void> update(Uri golden, Uint8List imageBytes) =>
super.update(golden, imageBytes);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/src/gradient_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'mocks.dart';
void main() {
testWidgets(
'control test',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
await tester.pumpWidget(
MaterialApp(
home: StreamChat(
client: client,
child: const Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: GradientAvatar(name: 'demo user', userId: 'demo123'),
),
),
),
),
),
);
expect(find.byType(GradientAvatar), findsOneWidget);
},
);
testGoldens(
'golden test for the name "demo user"',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: GradientAvatar(name: 'demo user', userId: 'demo123'),
),
),
),
),
);
await screenMatchesGolden(tester, 'gradient_avatar_0');
},
);
testGoldens(
'golden test for the name "demo"',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: GradientAvatar(name: 'demo', userId: 'demo1'),
),
),
),
),
);
await screenMatchesGolden(tester, 'gradient_avatar_1');
},
);
testGoldens(
'control special character test',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: GradientAvatar(
name: 'd123@/d de:\$as',
userId: 'demo123',
),
),
),
),
),
);
await screenMatchesGolden(tester, 'gradient_avatar_2');
},
);
testGoldens(
'control special character test 2',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'),
),
),
),
),
);
await screenMatchesGolden(tester, 'gradient_avatar_3');
},
);
}
@@ -80,7 +80,7 @@ void main() {
});
const messageText = '''
a message.
a message.
with multiple lines
and a list:
- a. okasd
@@ -6,32 +6,8 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'mocks.dart';
import 'simple_frame.dart';
void main() {
testGoldens(
'it should show no reactions',
(WidgetTester tester) async {
await tester.pumpWidgetBuilder(
SimpleFrame(
child: StreamChatTheme(
data: StreamChatThemeData(),
child: const SizedBox(
child: ReactionBubble(
reactions: [],
borderColor: Colors.black,
backgroundColor: Colors.white,
maskColor: Colors.white,
),
),
),
),
surfaceSize: const Size(100, 100),
);
await screenMatchesGolden(tester, 'reaction_bubble_0');
},
);
testGoldens(
'it should show a like - light theme',
(WidgetTester tester) async {
@@ -1,3 +1,8 @@
## Upcoming
✅ Added
- Added `MessageListCore.paginationLimit`
## 2.0.0
🛑️ Breaking Changes from `1.5.3`
@@ -71,6 +71,7 @@ class MessageListCore extends StatefulWidget {
this.parentMessage,
this.messageListController,
this.messageFilter,
this.paginationLimit = 20,
}) : super(key: key);
/// A [MessageListController] allows pagination.
@@ -86,6 +87,9 @@ class MessageListCore extends StatefulWidget {
/// Function used to build an empty widget
final WidgetBuilder emptyBuilder;
/// Limit used to paginate messages
final int paginationLimit;
/// Callback triggered when an error occurs while performing the given
/// request.
///
@@ -163,13 +167,20 @@ class MessageListCoreState extends State<MessageListCore> {
/// Fetches more messages with updated pagination and updates the widget.
///
/// Optionally pass the fetch direction, defaults to [QueryDirection.top]
/// Optionally pass a limit, defaults to 20
Future<void> paginateData({
QueryDirection direction = QueryDirection.top,
}) {
if (!_isThreadConversation) {
return _streamChannel!.queryMessages(direction: direction);
return _streamChannel!.queryMessages(
direction: direction,
limit: widget.paginationLimit,
);
} else {
return _streamChannel!.getReplies(widget.parentMessage!.id);
return _streamChannel!.getReplies(
widget.parentMessage!.id,
limit: widget.paginationLimit,
);
}
}
@@ -179,7 +190,10 @@ class MessageListCoreState extends State<MessageListCore> {
if (newStreamChannel != _streamChannel) {
if (_streamChannel == null /*only first time*/ && _isThreadConversation) {
newStreamChannel.getReplies(widget.parentMessage!.id);
newStreamChannel.getReplies(
widget.parentMessage!.id,
limit: widget.paginationLimit,
);
}
_streamChannel = newStreamChannel;
}
@@ -197,7 +211,10 @@ class MessageListCoreState extends State<MessageListCore> {
if (widget.parentMessage?.id != widget.parentMessage?.id) {
if (_isThreadConversation) {
_streamChannel!.getReplies(widget.parentMessage!.id);
_streamChannel!.getReplies(
widget.parentMessage!.id,
limit: widget.paginationLimit,
);
}
}
}
@@ -147,9 +147,18 @@ class StreamChannelState extends State<StreamChannel> {
}
/// Calls [channel.query] updating [queryMessage] stream
Future<void> queryMessages({QueryDirection? direction = QueryDirection.top}) {
if (direction == QueryDirection.top) return _queryTopMessages();
return _queryBottomMessages();
Future<void> queryMessages({
QueryDirection? direction = QueryDirection.top,
int limit = 20,
}) {
if (direction == QueryDirection.top) {
return _queryTopMessages(
limit: limit,
);
}
return _queryBottomMessages(
limit: limit,
);
}
/// Calls [channel.getReplies] updating [queryMessage] stream
@@ -151,7 +151,9 @@ void main() {
(tester) async {
const messageListCoreKey = Key('messageListCore');
final controller = MessageListController();
const paginationLimit = 10;
final messageListCore = MessageListCore(
paginationLimit: paginationLimit,
key: messageListCoreKey,
messageListBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
@@ -165,10 +167,6 @@ void main() {
final mockChannel = MockChannel();
when(() => mockChannel.state.isUpToDate).thenReturn(true);
// when(() => mockChannel.query(
// messagesPagination: any(named: 'messagesPagination'),
// preferOffline: any(named: 'preferOffline'),
// )).thenAnswer((_) => mockChannel.state);
final messages = _generateMessages();
when(() => mockChannel.state.messages).thenReturn(messages);
when(() => mockChannel.state.messagesStream)
@@ -191,7 +189,10 @@ void main() {
await coreState.paginateData();
verify(() => mockChannel.query(
messagesPagination: any(named: 'messagesPagination'),
messagesPagination: any(
named: 'messagesPagination',
that: wrapMatcher((it) => it.limit == paginationLimit),
),
preferOffline: any(named: 'preferOffline'),
)).called(1);
},
@@ -1,5 +1,3 @@
import 'dart:math' as math;
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
@@ -84,9 +82,9 @@ void main() {
cid: cids[index],
createdBy: users[index],
config: ChannelConfig(),
extraData: {'test_custom_field': math.Random().nextInt(100)},
extraData: {'test_custom_field': 3 + index},
createdAt: now,
memberCount: math.Random().nextInt(100),
memberCount: 3 + index,
lastMessageAt: now.add(Duration(hours: index)),
),
).reversed.toList(growable: false);
@@ -99,6 +97,8 @@ void main() {
}
group('getChannels', () {
tearDown(() async => database.flush());
final filter = Filter.in_('members', const ['testUserId']);
test('should return empty list of channels', () async {