Merge remote-tracking branch 'origin/develop' into cds-328
# Conflicts: # packages/stream_chat/CHANGELOG.md
@@ -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 }}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
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 {
|
||||
|
||||