Merge branch 'develop' into message-input-controller

This commit is contained in:
Salvatore Giordano
2021-11-30 15:27:40 +01:00
61 changed files with 803 additions and 371 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: flutter pub global activate melos 1.0.0-dev.3
run: flutter pub global activate melos 1.0.0-dev.10
- name: "Bootstrap Workspace"
run: melos bootstrap
@@ -31,7 +31,7 @@ jobs:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: |
flutter pub global activate melos 1.0.0-dev.3
flutter pub global activate melos 1.0.0-dev.10
- name: "Bootstrap Workspace"
run: melos bootstrap
- name: "Dart Analyze"
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 259 KiB

@@ -10,7 +10,7 @@ Customizing Message Actions
Message actions pop up in message overlay, when you long-press a message.
![](../assets/message_actions.png)
![](../assets/message_actions.jpg)
We have provided granular control over these actions.
+15 -1
View File
@@ -1,3 +1,17 @@
## 3.3.0
✅ Added
- Extra properties added to `PaginationParams` to aid in fetching messages.
- Added hard delete functionality.
🐞 Fixed
- `closeConnection()` now uses `normalClosure` status when closing websocket.
- Fixed local unread count indicator increasing for thread replies.
- Fixed user presence indicator not updating correctly.
- `ChannelEvent.membersCount` defaults to 0 avoiding parsing errors due to missing `members_count` field.
## 3.2.0
🐞 Fixed
@@ -720,4 +734,4 @@
## 0.0.2
- first beta version
- first beta version
+2 -2
View File
@@ -233,10 +233,10 @@ class _MessageViewState extends State<MessageView> {
),
),
),
)
),
],
),
)
),
],
);
}
@@ -648,7 +648,7 @@ class Channel {
}
/// Deletes the [message] from the channel.
Future<EmptyResponse> deleteMessage(Message message) async {
Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async {
// Directly deleting the local messages which are not yet sent to server
if (message.status == MessageSendingStatus.sending ||
message.status == MessageSendingStatus.failed) {
@@ -675,7 +675,7 @@ class Channel {
state?.addMessage(message);
final response = await _client.deleteMessage(message.id);
final response = await _client.deleteMessage(message.id, hard: hard);
state?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
@@ -1603,7 +1603,7 @@ class ChannelClientState {
message.createdAt.isBefore(
DateTime.now().subtract(
const Duration(
seconds: 1,
seconds: 5,
),
),
),
@@ -1663,7 +1663,11 @@ class ChannelClientState {
void _listenMessageDeleted() {
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
final message = event.message!;
addMessage(message);
if (event.hardDelete == true) {
removeMessage(message, hardDelete: true);
} else {
addMessage(message);
}
}));
}
@@ -1718,7 +1722,7 @@ class ChannelClientState {
}
/// Remove a [message] from this [channelState].
void removeMessage(Message message) {
void removeMessage(Message message, {bool hardDelete = false}) {
final parentId = message.parentId;
// i.e. it's a thread message
// 1. Remove the thread message
@@ -1740,7 +1744,10 @@ class ChannelClientState {
} else {
// Remove regular message
final allMessages = [...messages];
if (allMessages.remove(message)) {
if (hardDelete) {
allMessages.removeWhere((e) => e.id == message.id);
_channelState = _channelState.copyWith(messages: allMessages);
} else if (allMessages.remove(message)) {
_channelState = _channelState.copyWith(messages: allMessages);
}
}
@@ -1860,10 +1867,13 @@ class ChannelClientState {
(m) => m.user.id == message.user?.id,
) !=
null;
final isThreadMessage = message.parentId != null;
return !message.silent &&
!message.shadowed &&
message.user?.id != userId &&
!userIsMuted;
!userIsMuted &&
!isThreadMessage;
}
/// Update threads with updated information about messages.
@@ -44,10 +44,6 @@ 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
@@ -86,7 +82,7 @@ class StreamChatClient {
location: location,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: {'X-Stream-Client': _userAgent},
headers: {'X-Stream-Client': defaultUserAgent},
);
_chatApi = chatApi ??
@@ -106,7 +102,7 @@ class StreamChatClient {
tokenManager: _tokenManager,
handler: handleEvent,
logger: detachedLogger('🔌'),
queryParameters: {'X-Stream-Client': _userAgent},
queryParameters: {'X-Stream-Client': defaultUserAgent},
);
_retryPolicy = retryPolicy ??
@@ -131,6 +127,14 @@ class StreamChatClient {
_originalChatPersistenceClient = value;
}
/// Default user agent for all requests
static String defaultUserAgent = 'stream-chat-dart-client-'
'${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
/// Additionals headers for all requests
static Map<String, Object?> additionalHeaders = {};
ChatPersistenceClient? _originalChatPersistenceClient;
/// Chat persistence client
@@ -1209,8 +1213,14 @@ class StreamChatClient {
);
/// Deletes the given message
Future<EmptyResponse> deleteMessage(String messageId) =>
_chatApi.message.deleteMessage(messageId);
Future<EmptyResponse> deleteMessage(String messageId, {bool? hard}) async {
final response =
await _chatApi.message.deleteMessage(messageId, hard: hard);
if (hard == true) {
await _chatPersistenceClient?.deleteMessageById(messageId);
}
return response;
}
/// Get a message by [messageId]
Future<GetMessageResponse> getMessage(String messageId) =>
@@ -1428,6 +1438,7 @@ class ClientState {
.listen((Event event) async {
final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
channels[eventChannel.cid]?.dispose();
channels = channels..remove(eventChannel.cid);
}));
}
@@ -80,10 +80,16 @@ class MessageApi {
/// Deletes the given [messageId]
Future<EmptyResponse> deleteMessage(
String messageId,
) async {
String messageId, {
bool? hard,
}) async {
final response = await _client.delete(
'/messages/$messageId',
queryParameters: hard != null
? {
'hard': hard,
}
: null,
);
return EmptyResponse.fromJson(response.data);
}
@@ -60,8 +60,11 @@ class PaginationParams extends Equatable {
/// ```
const PaginationParams({
this.limit = 10,
this.before = 10,
this.after = 10,
this.offset,
this.next,
this.idAround,
this.greaterThan,
this.greaterThanOrEqual,
this.lessThan,
@@ -78,12 +81,22 @@ class PaginationParams extends Equatable {
/// The amount of items requested from the APIs.
final int limit;
/// The amount of items requested before message ID from the APIs.
final int before;
/// The amount of items requested after message ID from the APIs.
final int after;
/// The offset of requesting items.
final int? offset;
/// A key used to paginate.
final String? next;
/// Message ID to fetch messages around
@JsonKey(name: 'id_around')
final String? idAround;
/// Filter on ids greater than the given value.
@JsonKey(name: 'id_gt')
final String? greaterThan;
@@ -106,7 +119,10 @@ class PaginationParams extends Equatable {
/// Creates a copy of [PaginationParams] with specified attributes overridden.
PaginationParams copyWith({
int? limit,
int? before,
int? after,
int? offset,
String? idAround,
String? next,
String? greaterThan,
String? greaterThanOrEqual,
@@ -115,7 +131,10 @@ class PaginationParams extends Equatable {
}) =>
PaginationParams(
limit: limit ?? this.limit,
before: before ?? this.before,
after: limit ?? this.after,
offset: offset ?? this.offset,
idAround: idAround ?? this.idAround,
next: next ?? this.next,
greaterThan: greaterThan ?? this.greaterThan,
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
@@ -126,8 +145,11 @@ class PaginationParams extends Equatable {
@override
List<Object?> get props => [
limit,
before,
after,
offset,
next,
idAround,
greaterThan,
greaterThanOrEqual,
lessThan,
@@ -21,8 +21,11 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
PaginationParams(
limit: json['limit'] as int? ?? 10,
before: json['before'] as int? ?? 10,
after: json['after'] as int? ?? 10,
offset: json['offset'] as int?,
next: json['next'] as String?,
idAround: json['id_around'] as String?,
greaterThan: json['id_gt'] as String?,
greaterThanOrEqual: json['id_gte'] as String?,
lessThan: json['id_lt'] as String?,
@@ -32,6 +35,8 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{
'limit': instance.limit,
'before': instance.before,
'after': instance.after,
};
void writeNotNull(String key, dynamic value) {
@@ -42,6 +47,7 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
writeNotNull('offset', instance.offset);
writeNotNull('next', instance.next);
writeNotNull('id_around', instance.idAround);
writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan);
@@ -0,0 +1,17 @@
import 'package:dio/dio.dart';
import 'package:stream_chat/stream_chat.dart';
/// Interceptor that sets additional headers for all requests.
class AdditionalHeadersInterceptor extends Interceptor {
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
options.headers = {
...options.headers,
...StreamChatClient.additionalHeaders,
};
return handler.next(options);
}
}
@@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
@@ -41,6 +42,7 @@ class StreamHttpClient {
..._options.headers,
}
..interceptors.addAll([
AdditionalHeadersInterceptor(),
if (tokenManager != null) AuthInterceptor(this, tokenManager),
if (connectionIdManager != null)
ConnectionIdInterceptor(connectionIdManager),
@@ -14,7 +14,7 @@ final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
switch (json['runtimeType'] as String?) {
switch (json['runtimeType']) {
case 'preparing':
return Preparing.fromJson(json);
case 'inProgress':
@@ -55,7 +55,7 @@ class _$UploadStateTearOff {
);
}
UploadState fromJson(Map<String, Object> json) {
UploadState fromJson(Map<String, Object?> json) {
return UploadState.fromJson(json);
}
}
@@ -153,11 +153,14 @@ class _$PreparingCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$Preparing implements Preparing {
const _$Preparing();
const _$Preparing({String? $type}) : $type = $type ?? 'preparing';
factory _$Preparing.fromJson(Map<String, dynamic> json) =>
_$$PreparingFromJson(json);
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'UploadState.preparing()';
@@ -165,7 +168,8 @@ class _$Preparing implements Preparing {
@override
bool operator ==(dynamic other) {
return identical(this, other) || (other is Preparing);
return identical(this, other) ||
(other.runtimeType == runtimeType && other is Preparing);
}
@override
@@ -247,7 +251,7 @@ class _$Preparing implements Preparing {
@override
Map<String, dynamic> toJson() {
return _$$PreparingToJson(this)..['runtimeType'] = 'preparing';
return _$$PreparingToJson(this);
}
}
@@ -295,7 +299,9 @@ class _$InProgressCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$InProgress implements InProgress {
const _$InProgress({required this.uploaded, required this.total});
const _$InProgress(
{required this.uploaded, required this.total, String? $type})
: $type = $type ?? 'inProgress';
factory _$InProgress.fromJson(Map<String, dynamic> json) =>
_$$InProgressFromJson(json);
@@ -305,6 +311,9 @@ class _$InProgress implements InProgress {
@override
final int total;
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'UploadState.inProgress(uploaded: $uploaded, total: $total)';
@@ -313,19 +322,15 @@ class _$InProgress implements InProgress {
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is InProgress &&
(other.runtimeType == runtimeType &&
other is InProgress &&
(identical(other.uploaded, uploaded) ||
const DeepCollectionEquality()
.equals(other.uploaded, uploaded)) &&
(identical(other.total, total) ||
const DeepCollectionEquality().equals(other.total, total)));
other.uploaded == uploaded) &&
(identical(other.total, total) || other.total == total));
}
@override
int get hashCode =>
runtimeType.hashCode ^
const DeepCollectionEquality().hash(uploaded) ^
const DeepCollectionEquality().hash(total);
int get hashCode => Object.hash(runtimeType, uploaded, total);
@JsonKey(ignore: true)
@override
@@ -408,7 +413,7 @@ class _$InProgress implements InProgress {
@override
Map<String, dynamic> toJson() {
return _$$InProgressToJson(this)..['runtimeType'] = 'inProgress';
return _$$InProgressToJson(this);
}
}
@@ -419,8 +424,8 @@ abstract class InProgress implements UploadState {
factory InProgress.fromJson(Map<String, dynamic> json) =
_$InProgress.fromJson;
int get uploaded => throw _privateConstructorUsedError;
int get total => throw _privateConstructorUsedError;
int get uploaded;
int get total;
@JsonKey(ignore: true)
$InProgressCopyWith<InProgress> get copyWith =>
throw _privateConstructorUsedError;
@@ -445,11 +450,14 @@ class _$SuccessCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$Success implements Success {
const _$Success();
const _$Success({String? $type}) : $type = $type ?? 'success';
factory _$Success.fromJson(Map<String, dynamic> json) =>
_$$SuccessFromJson(json);
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'UploadState.success()';
@@ -457,7 +465,8 @@ class _$Success implements Success {
@override
bool operator ==(dynamic other) {
return identical(this, other) || (other is Success);
return identical(this, other) ||
(other.runtimeType == runtimeType && other is Success);
}
@override
@@ -539,7 +548,7 @@ class _$Success implements Success {
@override
Map<String, dynamic> toJson() {
return _$$SuccessToJson(this)..['runtimeType'] = 'success';
return _$$SuccessToJson(this);
}
}
@@ -581,7 +590,8 @@ class _$FailedCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$Failed implements Failed {
const _$Failed({required this.error});
const _$Failed({required this.error, String? $type})
: $type = $type ?? 'failed';
factory _$Failed.fromJson(Map<String, dynamic> json) =>
_$$FailedFromJson(json);
@@ -589,6 +599,9 @@ class _$Failed implements Failed {
@override
final String error;
@JsonKey(name: 'runtimeType')
final String $type;
@override
String toString() {
return 'UploadState.failed(error: $error)';
@@ -597,14 +610,13 @@ class _$Failed implements Failed {
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is Failed &&
(identical(other.error, error) ||
const DeepCollectionEquality().equals(other.error, error)));
(other.runtimeType == runtimeType &&
other is Failed &&
(identical(other.error, error) || other.error == error));
}
@override
int get hashCode =>
runtimeType.hashCode ^ const DeepCollectionEquality().hash(error);
int get hashCode => Object.hash(runtimeType, error);
@JsonKey(ignore: true)
@override
@@ -687,7 +699,7 @@ class _$Failed implements Failed {
@override
Map<String, dynamic> toJson() {
return _$$FailedToJson(this)..['runtimeType'] = 'failed';
return _$$FailedToJson(this);
}
}
@@ -696,7 +708,7 @@ abstract class Failed implements UploadState {
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
String get error => throw _privateConstructorUsedError;
String get error;
@JsonKey(ignore: true)
$FailedCopyWith<Failed> get copyWith => throw _privateConstructorUsedError;
}
@@ -22,31 +22,42 @@ Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
'size': instance.size,
};
_$Preparing _$$PreparingFromJson(Map<String, dynamic> json) => _$Preparing();
_$Preparing _$$PreparingFromJson(Map<String, dynamic> json) => _$Preparing(
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$PreparingToJson(_$Preparing instance) =>
<String, dynamic>{};
<String, dynamic>{
'runtimeType': instance.$type,
};
_$InProgress _$$InProgressFromJson(Map<String, dynamic> json) => _$InProgress(
uploaded: json['uploaded'] as int,
total: json['total'] as int,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$InProgressToJson(_$InProgress instance) =>
<String, dynamic>{
'uploaded': instance.uploaded,
'total': instance.total,
'runtimeType': instance.$type,
};
_$Success _$$SuccessFromJson(Map<String, dynamic> json) => _$Success();
_$Success _$$SuccessFromJson(Map<String, dynamic> json) => _$Success(
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$SuccessToJson(_$Success instance) =>
<String, dynamic>{};
Map<String, dynamic> _$$SuccessToJson(_$Success instance) => <String, dynamic>{
'runtimeType': instance.$type,
};
_$Failed _$$FailedFromJson(Map<String, dynamic> json) => _$Failed(
error: json['error'] as String,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$FailedToJson(_$Failed instance) => <String, dynamic>{
'error': instance.error,
'runtimeType': instance.$type,
};
@@ -27,6 +27,7 @@ class Event {
this.channelId,
this.channelType,
this.parentId,
this.hardDelete,
this.extraData = const {},
this.isLocal = true,
}) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc();
@@ -91,6 +92,10 @@ class Event {
@JsonKey(defaultValue: false)
final bool isLocal;
/// This is true if the message has been hard deleted
@JsonKey(includeIfNull: false)
final bool? hardDelete;
/// Map of custom channel extraData
final Map<String, Object?> extraData;
@@ -113,6 +118,7 @@ class Event {
'channel_id',
'channel_type',
'parent_id',
'hard_delete',
'is_local',
];
@@ -139,6 +145,7 @@ class Event {
int? unreadChannels,
bool? online,
String? parentId,
bool? hardDelete,
Map<String, Object?>? extraData,
}) =>
Event(
@@ -158,6 +165,7 @@ class Event {
channelId: channelId ?? this.channelId,
channelType: channelType ?? this.channelType,
parentId: parentId ?? this.parentId,
hardDelete: hardDelete ?? this.hardDelete,
extraData: extraData ?? this.extraData,
isLocal: isLocal,
);
@@ -181,7 +189,7 @@ class EventChannel extends ChannelModel {
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
required int memberCount,
int memberCount = 0,
Map<String, Object?>? extraData,
int cooldown = 0,
String? team,
@@ -37,30 +37,42 @@ Event _$EventFromJson(Map<String, dynamic> json) => Event(
channelId: json['channel_id'] as String?,
channelType: json['channel_type'] as String?,
parentId: json['parent_id'] as String?,
hardDelete: json['hard_delete'] as bool?,
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
isLocal: json['is_local'] as bool? ?? false,
);
Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
'type': instance.type,
'cid': instance.cid,
'channel_id': instance.channelId,
'channel_type': instance.channelType,
'connection_id': instance.connectionId,
'created_at': instance.createdAt.toIso8601String(),
'me': instance.me?.toJson(),
'user': instance.user?.toJson(),
'message': instance.message?.toJson(),
'channel': instance.channel?.toJson(),
'member': instance.member?.toJson(),
'reaction': instance.reaction?.toJson(),
'total_unread_count': instance.totalUnreadCount,
'unread_channels': instance.unreadChannels,
'online': instance.online,
'parent_id': instance.parentId,
'is_local': instance.isLocal,
'extra_data': instance.extraData,
};
Map<String, dynamic> _$EventToJson(Event instance) {
final val = <String, dynamic>{
'type': instance.type,
'cid': instance.cid,
'channel_id': instance.channelId,
'channel_type': instance.channelType,
'connection_id': instance.connectionId,
'created_at': instance.createdAt.toIso8601String(),
'me': instance.me?.toJson(),
'user': instance.user?.toJson(),
'message': instance.message?.toJson(),
'channel': instance.channel?.toJson(),
'member': instance.member?.toJson(),
'reaction': instance.reaction?.toJson(),
'total_unread_count': instance.totalUnreadCount,
'unread_channels': instance.unreadChannels,
'online': instance.online,
'parent_id': instance.parentId,
'is_local': instance.isLocal,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('hard_delete', instance.hardDelete);
val['extra_data'] = instance.extraData;
return val;
}
EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
members: (json['members'] as List<dynamic>?)
@@ -82,7 +94,7 @@ EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
memberCount: json['member_count'] as int,
memberCount: json['member_count'] as int? ?? 0,
extraData: json['extra_data'] as Map<String, dynamic>?,
cooldown: json['cooldown'] as int? ?? 0,
team: json['team'] as String?,
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/user.dart';
@@ -5,9 +6,9 @@ part 'read.g.dart';
/// The class that defines a read event
@JsonSerializable()
class Read {
class Read extends Equatable {
/// Constructor used for json serialization
Read({
const Read({
required this.lastRead,
required this.user,
this.unreadMessages = 0,
@@ -39,4 +40,11 @@ class Read {
user: user ?? this.user,
unreadMessages: unreadMessages ?? this.unreadMessages,
);
@override
List<Object?> get props => [
lastRead,
user,
unreadMessages,
];
}
@@ -179,5 +179,14 @@ class User extends Equatable {
);
@override
List<Object?> get props => [id, role];
List<Object?> get props => [
id,
role,
lastActive,
online,
extraData,
banned,
teams,
language,
];
}
@@ -82,7 +82,6 @@ abstract class ChatPersistenceClient {
members: data[0] as List<Member>,
// ignore: cast_nullable_to_non_nullable
read: data[1] as List<Read>,
// ignore: cast_nullable_to_non_nullable
channel: data[2] as ChannelModel?,
// ignore: cast_nullable_to_non_nullable
messages: data[3] as List<Message>,
@@ -121,7 +121,8 @@ class WebSocket with TimerHelper {
_logger?.info('Closing connection with $baseUrl');
if (_webSocketChannel != null) {
_unsubscribeFromWebSocketChannel();
_webSocketChannel?.sink.close(status.goingAway);
_webSocketChannel?.sink
.close(_manuallyClosed ? status.normalClosure : status.goingAway);
_webSocketChannel = null;
}
}
@@ -309,7 +310,10 @@ class WebSocket with TimerHelper {
Event? event;
try {
event = Event.fromJson(jsonData);
} catch (_) {}
} catch (e, stk) {
_logger?.warning('Error parsing an event: $e');
_logger?.warning('Stack trace: $stk');
}
if (event == null) return;
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '3.2.0';
const PACKAGE_VERSION = '3.3.0';
+4 -4
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 3.2.0
version: 3.3.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -13,10 +13,10 @@ dependencies:
collection: ^1.15.0
dio: ^4.0.0
equatable: ^2.0.0
freezed_annotation: ^0.15.0
freezed_annotation: ^1.0.0
http_parser: ^4.0.0
jose: ^0.3.2
json_annotation: ^4.0.1
json_annotation: ^4.3.0
logging: ^1.0.1
meta: ^1.3.0
mime: ^1.0.0
@@ -28,7 +28,7 @@ dependencies:
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
freezed: ^0.15.0+1
freezed: ^1.0.0
json_serializable: ^6.0.1
mocktail: ^0.2.0
test: ^1.17.12
@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
void main() {
late AdditionalHeadersInterceptor additionalHeadersInterceptor;
setUp(() {
additionalHeadersInterceptor = AdditionalHeadersInterceptor();
});
test(
'`onRequest` should add additional headers in the request',
() async {
final options = RequestOptions(path: 'test-path');
final handler = RequestInterceptorHandler();
StreamChatClient.additionalHeaders = {'test-header': 'test-value'};
additionalHeadersInterceptor.onRequest(options, handler);
final updatedOptions = (await handler.future).data as RequestOptions;
final updateHeaders = updatedOptions.headers;
expect(updateHeaders.containsKey('test-header'), isTrue);
expect(updateHeaders['test-header'], 'test-value');
},
);
}
@@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
@@ -48,12 +49,23 @@ void main() {
return dioError;
}
test('UserAgentInterceptor should be added', () {
const apiKey = 'api-key';
final client = StreamHttpClient(apiKey);
expect(
client.httpClient.interceptors
.whereType<AdditionalHeadersInterceptor>()
.length,
1);
});
test('AuthInterceptor should be added if tokenManager is provided', () {
const apiKey = 'api-key';
final client = StreamHttpClient(apiKey, tokenManager: TokenManager());
expect(client.httpClient.interceptors.length, 1);
expect(client.httpClient.interceptors.first, isA<AuthInterceptor>());
expect(
client.httpClient.interceptors.whereType<AuthInterceptor>().length, 1);
});
test(
@@ -65,10 +77,11 @@ void main() {
connectionIdManager: ConnectionIdManager(),
);
expect(client.httpClient.interceptors.length, 1);
expect(
client.httpClient.interceptors.first,
isA<ConnectionIdInterceptor>(),
client.httpClient.interceptors
.whereType<ConnectionIdInterceptor>()
.length,
1,
);
},
);
@@ -80,10 +93,9 @@ void main() {
logger: Logger('test-logger'),
);
expect(client.httpClient.interceptors.length, 1);
expect(
client.httpClient.interceptors.first,
isA<LoggingInterceptor>(),
client.httpClient.interceptors.whereType<LoggingInterceptor>().length,
1,
);
});
@@ -111,27 +123,6 @@ void main() {
verify(() => logger.severe(any())).called(greaterThan(0));
});
test('`.lock` should lock the dio client', () async {
final client = StreamHttpClient('api-key');
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
client.lock();
expect(client.httpClient.interceptors.requestLock.locked, isTrue);
});
test('`.unlock` should unlock the dio client', () async {
final client = StreamHttpClient('api-key');
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
client.lock();
expect(client.httpClient.interceptors.requestLock.locked, isTrue);
client.unlock();
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
});
test('`.clear` should clear and unlock the dio client', () async {
final client = StreamHttpClient('api-key')..clear();
expect(client.httpClient.interceptors.requestLock.locked, isFalse);
});
test('`.close` should close the dio client', () async {
final client = StreamHttpClient('api-key')..close(force: true);
try {
@@ -70,13 +70,19 @@ void main() {
expect(
newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'});
final newUserCreateTime = DateTime.now();
newReaction = reaction.copyWith(
type: 'lol',
createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'),
extraData: {},
messageId: 'test',
score: 2,
user: User(id: 'test'),
user: User(
id: 'test',
createdAt: newUserCreateTime,
updatedAt: newUserCreateTime,
),
userId: 'test',
);
@@ -88,12 +94,21 @@ void main() {
expect(newReaction.extraData, {});
expect(newReaction.messageId, 'test');
expect(newReaction.score, 2);
expect(newReaction.user, User(id: 'test'));
expect(
newReaction.user,
User(
id: 'test',
createdAt: newUserCreateTime,
updatedAt: newUserCreateTime,
),
);
expect(newReaction.userId, 'test');
});
test('merge', () {
final reaction = Reaction.fromJson(jsonFixture('reaction.json'));
final newUserCreateTime = DateTime.now();
final newReaction = reaction.merge(
Reaction(
type: 'lol',
@@ -101,7 +116,11 @@ void main() {
extraData: {},
messageId: 'test',
score: 2,
user: User(id: 'test'),
user: User(
id: 'test',
createdAt: newUserCreateTime,
updatedAt: newUserCreateTime,
),
userId: 'test',
),
);
@@ -114,7 +133,14 @@ void main() {
expect(newReaction.extraData, {});
expect(newReaction.messageId, 'test');
expect(newReaction.score, 2);
expect(newReaction.user, User(id: 'test'));
expect(
newReaction.user,
User(
id: 'test',
createdAt: newUserCreateTime,
updatedAt: newUserCreateTime,
),
);
expect(newReaction.userId, 'test');
});
});
+22 -4
View File
@@ -6,18 +6,36 @@
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
## 3.3.0
✅ Added
- `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`.
- `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call.
- Added `MessageListView.keyboardDismissBehavior` property.
🐞 Fixed
- [[#766]]`AttachmentActionsModal` now has customisation options for actions.
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
- Fixed adding attachments on web.
- [[#767]](https://github.com/GetStream/stream-chat-flutter/issues/767): Fix `MessageInput` focus behaviour when sending messages.
- Fixed user presence indicator not updating correctly.
- Do not use `withData: true` in `FilePicker` calls.
- Fixed read indicator not updating correctly in specific situations.
## 3.2.0
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`.
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🐞 Fixed
- Fixed message highlight animation alignment in `MessageListView`
- Fixed message highlight animation alignment in `MessageListView`.
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
- Fixed `MessageListView` initialIndex not working in some cases.
- Improved `MessageListView` rendering in case of reordering.
- Fix image thumbnail generation when using Stream CDN
- Fix image thumbnail generation when using Stream CDN.
✅ Added
@@ -868,4 +886,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
## 0.0.1
- First release
- First release
@@ -64,11 +64,12 @@ class MyApp extends StatelessWidget {
),
),
messageListViewTheme: const MessageListViewThemeData(
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
)),
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
),
),
otherMessageTheme: MessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle(
@@ -45,6 +45,7 @@ class PositionedList extends StatefulWidget {
this.addSemanticIndexes = true,
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
this.keyboardDismissBehavior,
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
'positionedIndex cannot be 0 and must be smaller than itemCount'),
super(key: key);
@@ -134,6 +135,10 @@ class PositionedList extends StatefulWidget {
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
final bool addAutomaticKeepAlives;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
@override
State<StatefulWidget> createState() => _PositionedListState();
}
@@ -173,6 +178,7 @@ class _PositionedListState extends State<PositionedList> {
anchor: widget.alignment,
center: _centerKey,
controller: scrollController,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: widget.cacheExtent,
@@ -28,9 +28,12 @@ class UnboundedCustomScrollView extends CustomScrollView {
List<Widget> slivers = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
}) : _anchor = anchor,
super(
key: key,
keyboardDismissBehavior: keyboardDismissBehavior ??
ScrollViewKeyboardDismissBehavior.manual,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
@@ -52,6 +52,7 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
separatorBuilder = null,
super(key: key);
@@ -77,6 +78,7 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
super(key: key);
@@ -92,6 +94,10 @@ class ScrollablePositionedList extends StatefulWidget {
/// index of the child element with that associated key, or null if not found.
final ChildIndexGetter? findChildIndexCallback;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
/// Number of items the [itemBuilder] can produce.
final int itemCount;
@@ -344,6 +350,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
child: NotificationListener<ScrollNotification>(
onNotification: (_) => _isTransitioning,
child: PositionedList(
keyboardDismissBehavior: widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
@@ -374,6 +381,8 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
child: NotificationListener<ScrollNotification>(
onNotification: (_) => false,
child: PositionedList(
keyboardDismissBehavior:
widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
@@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget {
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
this.showReply = true,
this.showShowInChat = true,
this.showSave = true,
this.showDelete = true,
this.customActions = const [],
}) : super(key: key);
/// The message containing the attachments
@@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to provide download files
final AttachmentDownloader? fileDownloader;
/// Show reply option
final bool showReply;
/// Show show in chat option
final bool showShowInChat;
/// Show save option
final bool showSave;
/// Show delete option
final bool showDelete;
/// List of custom actions
final List<AttachmentAction> customActions;
/// Creates a copy of [MessageWidget] with specified attributes overridden.
AttachmentActionsModal copyWith({
Key? key,
int? currentIndex,
Message? message,
VoidCallback? onShowMessage,
AttachmentDownloader? imageDownloader,
AttachmentDownloader? fileDownloader,
bool? showReply,
bool? showShowInChat,
bool? showSave,
bool? showDelete,
List<AttachmentAction>? customActions,
}) =>
AttachmentActionsModal(
key: key ?? this.key,
currentIndex: currentIndex ?? this.currentIndex,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
imageDownloader: imageDownloader ?? this.imageDownloader,
fileDownloader: fileDownloader ?? this.fileDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
showSave: showSave ?? this.showSave,
showDelete: showDelete ?? this.showDelete,
customActions: customActions ?? this.customActions,
);
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
@@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
context,
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
if (showReply)
_buildButton(
context,
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
),
_buildButton(
context,
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
if (showShowInChat)
_buildButton(
context,
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
),
onShowMessage,
),
onShowMessage,
),
_buildButton(
context,
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
if (showSave)
_buildButton(
context,
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image';
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
downloader(
attachment,
progressCallback: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
).catchError((e, stk) {
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
barrierColor: theme.colorTheme.overlay,
builder: (context) => _buildDownloadProgressDialog(
context,
progressNotifier,
),
);
},
),
() {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image';
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
downloader(
attachment,
progressCallback: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
).catchError((e, stk) {
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
barrierColor: theme.colorTheme.overlay,
builder: (context) => _buildDownloadProgressDialog(
context,
progressNotifier,
),
);
},
),
if (StreamChat.of(context).currentUser?.id ==
message.user?.id)
message.user?.id &&
showDelete)
_buildButton(
context,
context.translations.deleteLabel.capitalize(),
@@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget {
},
color: theme.colorTheme.accentError,
),
...customActions
.map(
(e) => _buildButton(
context,
e.actionTitle,
e.icon,
e.onTap,
),
)
.toList(),
]
.map<Widget>((e) => Align(
alignment: Alignment.centerRight,
@@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget {
Widget _buildButton(
context,
String title,
StreamSvgIcon icon,
Widget icon,
VoidCallback? onTap, {
Color? color,
Key? key,
@@ -331,3 +393,22 @@ class _DownloadProgress {
int get toPercentage => (received * 100) ~/ total;
}
/// Class for custom attachment action
class AttachmentAction {
/// Constructor for custom attachment action
AttachmentAction({
required this.actionTitle,
required this.icon,
required this.onTap,
});
/// Title for the attachment action
String actionTitle;
/// Icon for the attachment action
Widget icon;
/// Callback for when the action is tapped
VoidCallback onTap;
}
@@ -59,9 +59,10 @@ class ChannelInfo extends StatelessWidget {
final memberCount = channel.memberCount;
if (memberCount != null && memberCount > 2) {
var text = context.translations.membersCountText(memberCount);
final watcherCount = channel.state?.watcherCount ?? 0;
if (watcherCount > 0) {
text += ' ${context.translations.watchersCountText(watcherCount)}';
final onlineCount =
members?.where((m) => m.user?.online == true).length ?? 0;
if (onlineCount > 0) {
text += ', ${context.translations.watchersCountText(onlineCount)}';
}
alternativeWidget = Text(
text,
@@ -126,16 +126,26 @@ class ChannelPreview extends StatelessWidget {
streamChatState.currentUser?.id) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state!.read
.where((element) =>
element.user.id !=
channel.client.state.currentUser!.id)
.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
.isNotEmpty,
child: BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
builder: (context, data) {
final readList = data.where((it) =>
it.user.id !=
channel.client.state.currentUser?.id &&
(it.lastRead
.isAfter(lastMessage!.createdAt) ||
it.lastRead.isAtSameMomentAs(
lastMessage.createdAt,
)));
final isMessageRead = readList.length >=
(channel.memberCount ?? 0) - 1;
return SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: isMessageRead,
);
},
),
);
}
@@ -1,6 +1,7 @@
import 'package:characters/characters.dart';
import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/localization/translations.dart';
@@ -46,7 +47,7 @@ extension IterableX<T> on Iterable<T> {
extension PlatformFileX on PlatformFile {
/// Converts the [PlatformFile] into [AttachmentFile]
AttachmentFile get toAttachmentFile => AttachmentFile(
path: path,
path: kIsWeb ? null : path,
name: name,
bytes: bytes,
size: size,
@@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget {
this.startIndex = 0,
String? userName,
this.onShowMessage,
this.attachmentActionsModalBuilder,
}) : userName = userName ?? '',
super(key: key);
@@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget {
/// Callback for when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
}
@@ -196,6 +202,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
StreamChannel.of(context).channel,
);
},
attachmentActionsModalBuilder:
widget.attachmentActionsModalBuilder,
),
if (!widget.message.isEphemeral)
GalleryFooter(
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
@@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
typedef AttachmentActionsBuilder = Widget Function(
BuildContext context,
Attachment attachment,
AttachmentActionsModal defaultActionsModal,
);
/// Header/AppBar widget for media display screen
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
@@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
this.userName = '',
this.sentAt = '',
this.backgroundColor,
this.attachmentActionsModalBuilder,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color of this [GalleryHeader].
final Color? backgroundColor;
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
@@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
final galleryHeaderThemeData =
StreamChatTheme.of(context).galleryHeaderTheme;
final defaultModal = AttachmentActionsModal(
message: message,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
);
final effectiveModal = attachmentActionsModalBuilder?.call(
context,
message.attachments[currentIndex],
defaultModal,
) ??
defaultModal;
final result = await showDialog(
useRootNavigator: false,
context: context,
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
builder: (context) => StreamChannel(
channel: channel,
child: AttachmentActionsModal(
message: message,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
),
child: effectiveModal,
),
);
@@ -81,6 +81,7 @@ class GroupAvatar extends StatelessWidget {
),
initialData: member,
builder: (context, member) => UserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
),
@@ -118,6 +119,7 @@ class GroupAvatar extends StatelessWidget {
),
initialData: member,
builder: (context, member) => UserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
),
@@ -218,6 +218,7 @@ class MessageInput extends StatefulWidget {
this.mentionAllAppUsers = false,
this.attachmentsPickerBuilder,
this.sendButtonBuilder,
this.shouldKeepFocusAfterMessage,
}) : assert(
initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`",
@@ -342,6 +343,10 @@ class MessageInput extends StatefulWidget {
/// Builder for creating send button
final MessageRelatedBuilder? sendButtonBuilder;
/// Defines if the [MessageInput] loses focuses after a message is sent.
/// The default behaviour keeps focus until a command is enabled.
final bool? shouldKeepFocusAfterMessage;
@override
MessageInputState createState() => MessageInputState();
@@ -988,7 +993,8 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildMentionsOverlayEntry() {
if (messageInputController.selectionStart < 0) {
final channel = StreamChannel.of(context).channel;
if (messageInputController.selectionStart < 0 || channel.state == null) {
return const Offstage();
}
@@ -1020,7 +1026,7 @@ class MessageInputState extends State<MessageInput> {
query: query,
mentionAllAppUsers: widget.mentionAllAppUsers,
client: StreamChat.of(context).client,
channel: StreamChannel.of(context).channel,
channel: channel,
size: Size(renderObject.size.width - 16, 400),
mentionsTileBuilder: tileBuilder,
onMentionUserTap: (user) {
@@ -1476,7 +1482,6 @@ class MessageInputState extends State<MessageInput> {
}
final res = await FilePicker.platform.pickFiles(
type: type,
withData: true,
);
if (res?.files.isNotEmpty == true) {
file = res!.files.single.toAttachmentFile;
@@ -1554,7 +1559,9 @@ class MessageInputState extends State<MessageInput> {
return;
}
final shouldUnfocus = _commandEnabled;
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled;
if (_commandEnabled) {
text = '${'/${_chosenCommand!.name} '}$text';
@@ -1620,8 +1627,10 @@ class MessageInputState extends State<MessageInput> {
sendingFuture = channel.updateMessage(message);
}
if (!shouldUnfocus) {
if (shouldKeepFocus) {
FocusScope.of(context).requestFocus(_focusNode);
} else {
FocusScope.of(context).unfocus();
}
final resp = await sendingFuture;
@@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message);
/// Callback on reply tapped
typedef ReplyTapCallback = void Function(Message);
/// Spacing Types (These are properties of a message to help inform the decision
/// of how much space / which widget to build after it)
enum SpacingType {
/// Message is a thread
thread,
/// There is a >1s time diff between current and last message
timeDiff,
/// Next message is by a different user
otherUser,
/// Message is deleted
deleted,
/// No other conditions are valid, default spacing (This will likely be the
/// only rule in the list provided)
defaultSpacing,
}
/// Builder for building certain spacing after widgets.
/// This spacing can be in form of any widgets you like.
/// A List of [SpacingType] is provided to help inform the decision of
/// what to build after the message.
///
/// As an example:
/// MessageListView(
/// spacingWidgetBuilder: (context, list) {
/// if(list.contains(SpacingType.defaultSpacing)) {
/// return SizedBox(height: 2.0,);
/// } else {
/// return SizedBox(height: 8.0,);
/// }
/// },
/// ),
typedef SpacingWidgetBuilder = Widget Function(
BuildContext context,
List<SpacingType> spacingTypes,
);
/// Class for message details
// ignore: prefer-match-file-name
class MessageDetails {
@@ -171,8 +211,14 @@ class MessageListView extends StatefulWidget {
this.reverse = true,
this.paginationLimit = 20,
this.paginationLoadingIndicatorBuilder,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
this.spacingWidgetBuilder,
}) : super(key: key);
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// Function used to build a custom message widget
final MessageBuilder? messageBuilder;
@@ -289,6 +335,12 @@ class MessageListView extends StatefulWidget {
/// Builder used to build the loading indicator shown while paginating.
final WidgetBuilder? paginationLoadingIndicatorBuilder;
/// This allows a user to customise the space after a message
/// A List of [SpacingType] is provided to provide more data about the
/// type of message (thread, difference in time between current and last
/// message, default spacing, etc)
final SpacingWidgetBuilder? spacingWidgetBuilder;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -443,9 +495,6 @@ class _MessageListViewState extends State<MessageListView> {
childAnchor: Alignment.topCenter,
message: statusString,
child: LazyLoadScrollView(
onPageScrollStart: () {
FocusScope.of(context).unfocus();
},
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
@@ -471,6 +520,7 @@ class _MessageListViewState extends State<MessageListView> {
key: (initialIndex != 0 && initialAlignment != 0)
? ValueKey('$initialIndex-$initialAlignment')
: null,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
itemPositionsListener: _itemPositionListener,
initialScrollIndex: initialIndex,
initialAlignment: initialAlignment,
@@ -564,17 +614,38 @@ class _MessageListViewState extends State<MessageListView> {
Units.MINUTE,
);
final spacingRules = <SpacingType>[];
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);
final hasTimeDiff = timeDiff >= 1;
if (hasTimeDiff) {
spacingRules.add(SpacingType.timeDiff);
}
return const SizedBox(height: 2);
if (!isNextUserSame) {
spacingRules.add(SpacingType.otherUser);
}
if (isThread) {
spacingRules.add(SpacingType.thread);
}
if (isDeleted) {
spacingRules.add(SpacingType.deleted);
}
if (spacingRules.isNotEmpty) {
return widget.spacingWidgetBuilder
?.call(context, spacingRules) ??
const SizedBox(height: 8);
}
return widget.spacingWidgetBuilder
?.call(context, [SpacingType.defaultSpacing]) ??
const SizedBox(height: 2);
},
itemBuilder: (context, i) {
if (i == itemCount - 1) {
@@ -1003,15 +1074,6 @@ class _MessageListViewState extends State<MessageListView> {
);
}
final channel = streamChannel!.channel;
final readList = channel.state?.read.where((read) {
if (read.user.id == userId) return false;
return read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt);
}).toList() ??
[];
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final hasFileAttachment =
message.attachments.any((it) => it.type == 'file');
@@ -1140,8 +1202,6 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage
? _streamTheme.ownMessageTheme
: _streamTheme.otherMessageTheme,
readList: readList,
allRead: allRead,
onReturnAction: (action) {
switch (action) {
case ReturnActionType.none:
@@ -97,14 +97,20 @@ class MessageWidget extends StatefulWidget {
this.deletedBottomRowBuilder,
this.onReturnAction,
this.customAttachmentBuilders,
this.readList,
this.padding,
this.textPadding = const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
this.attachmentPadding = EdgeInsets.zero,
this.allRead = false,
@Deprecated('''
allRead is now deprecated and it will be removed in future releases.
The MessageWidget now listens for read events on its own.
''') this.allRead = false,
@Deprecated('''
readList is now deprecated and it will be removed in future releases.
The MessageWidget now listens for read events on its own.
''') this.readList,
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
@@ -508,7 +514,6 @@ class MessageWidget extends StatefulWidget {
showUserAvatar: showUserAvatar ?? this.showUserAvatar,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
showReactions: showReactions ?? this.showReactions,
allRead: allRead ?? this.allRead,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannelIndicator:
@@ -517,7 +522,6 @@ class MessageWidget extends StatefulWidget {
onLinkTap: onLinkTap ?? this.onLinkTap,
showReactionPickerIndicator:
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
readList: readList ?? this.readList,
onShowMessage: onShowMessage ?? this.onShowMessage,
onReturnAction: onReturnAction ?? this.onReturnAction,
showUsername: showUsername ?? this.showUsername,
@@ -558,8 +562,6 @@ class _MessageWidgetState extends State<MessageWidget>
bool get showTimeStamp => widget.showTimestamp;
bool get isMessageRead => widget.readList?.isNotEmpty == true;
bool get showInChannel => widget.showInChannelIndicator;
bool get hasQuotedMessage => widget.message.quotedMessage != null;
@@ -1230,6 +1232,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildSendingIndicator() {
final style = widget.messageTheme.createdAtStyle;
final message = widget.message;
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
if (hasNonUrlAttachments &&
(message.status == MessageSendingStatus.sending ||
@@ -1252,27 +1255,40 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
Widget child = SendingIndicator(
message: message,
isMessageRead: isMessageRead,
size: style!.fontSize,
final channel = StreamChannel.of(context).channel;
return BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
builder: (context, data) {
final readList = data.where((it) =>
it.user.id != _streamChat.currentUser?.id &&
(it.lastRead.isAfter(message.createdAt) ||
it.lastRead.isAtSameMomentAs(message.createdAt)));
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
Widget child = SendingIndicator(
message: message,
isMessageRead: isMessageRead,
size: style!.fontSize,
);
if (isMessageRead) {
child = Row(
children: [
if (memberCount > 2)
Text(
readList.length.toString(),
style: style.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
const SizedBox(width: 2),
child,
],
);
}
return child;
},
);
if (isMessageRead) {
child = Row(
children: [
if (StreamChannel.of(context).channel.memberCount! > 2)
Text(
widget.readList!.length.toString(),
style: style.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
const SizedBox(width: 2),
child,
],
);
}
return child;
}
Widget _buildUserAvatar() => Transform.translate(
@@ -110,7 +110,15 @@ class StreamChatState extends State<StreamChat> {
onBackgroundEventReceived: widget.onBackgroundEventReceived,
backgroundKeepAlive: widget.backgroundKeepAlive,
connectivityStream: widget.connectivityStream,
child: widget.child ?? const Offstage(),
child: Builder(
builder: (context) {
StreamChatClient.additionalHeaders = {
'X-Stream-Client':
'${StreamChatClient.defaultUserAgent}-ui',
};
return widget.child ?? const Offstage();
},
),
),
);
},
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
export 'src/attachment_actions_modal.dart';
export 'src/back_button.dart';
export 'src/channel_avatar.dart';
export 'src/channel_header.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 3.2.0
version: 3.3.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -1,7 +1,13 @@
# Upcoming
✅ Added
- Added `MessageInputController` to hold `Message` related data.
## 3.3.0
- Updated `stream_chat` dependency to [`3.3.0`](https://pub.dev/packages/stream_chat/changelog).
## 3.2.0
- Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -83,9 +83,12 @@ class HomeScreen extends StatelessWidget {
channelListController: channelListController,
filter: Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', [
StreamChatCore.of(context).currentUser!.id,
])
Filter.in_(
'members',
[
StreamChatCore.of(context).currentUser!.id,
],
)
]),
emptyBuilder: (BuildContext context) => const Center(
child: Text('Looks like you are not in any channels'),
@@ -120,7 +123,7 @@ class HomeScreen extends StatelessWidget {
itemBuilder: (BuildContext context, int index) {
final _item = channels[index];
return ListTile(
title: Text(_item.name!),
title: Text(_item.name ?? ''),
subtitle: StreamBuilder<Message?>(
stream: _item.state!.lastMessageStream,
initialData: _item.state!.lastMessage,
@@ -318,10 +321,10 @@ class _MessageScreenState extends State<MessageScreen> {
),
),
),
)
),
],
),
)
),
],
),
),
@@ -157,7 +157,10 @@ class ChannelListCoreState extends State<ChannelListCore> {
presence: widget.presence,
memberLimit: widget.memberLimit,
messageLimit: widget.messageLimit,
paginationParams: PaginationParams(limit: widget.limit),
paginationParams: PaginationParams(
limit: widget.limit,
offset: 0,
),
);
/// Fetches more channels with updated pagination and updates the widget
@@ -227,13 +227,13 @@ class StreamChannelState extends State<StreamChannel> {
preferOffline: preferOffline,
);
Future<List<ChannelState>> _queryAtMessage({
Future<ChannelState?> _queryAtMessage({
String? messageId,
int before = 20,
int after = 20,
bool preferOffline = false,
}) async {
if (channel.state == null) return [];
if (channel.state == null) return null;
channel.state!.isUpToDate = false;
channel.state!.truncate();
@@ -245,23 +245,33 @@ class StreamChannelState extends State<StreamChannel> {
preferOffline: preferOffline,
);
channel.state!.isUpToDate = true;
return [];
return null;
}
return Future.wait([
queryBeforeMessage(
messageId,
limit: before,
preferOffline: preferOffline,
),
queryAfterMessage(
messageId,
limit: after,
preferOffline: preferOffline,
),
]);
return queryAroundMessage(
messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
///
Future<ChannelState> queryAroundMessage(
String messageId, {
int before = 20,
int after = 20,
bool preferOffline = false,
}) =>
channel.query(
messagesPagination: PaginationParams(
idAround: messageId,
before: before,
after: after,
),
preferOffline: preferOffline,
);
///
Future<ChannelState> queryBeforeMessage(
String messageId, {
@@ -95,7 +95,12 @@ class StreamChatCoreState extends State<StreamChatCore>
Timer? _disconnectTimer;
@override
Widget build(BuildContext context) => widget.child;
Widget build(BuildContext context) {
StreamChatClient.additionalHeaders = {
'X-Stream-Client': '${StreamChatClient.defaultUserAgent}-core',
};
return widget.child;
}
// coverage:ignore-start
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 3.2.0
version: 3.3.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -16,7 +16,7 @@ dependencies:
sdk: flutter
meta: ^1.3.0
rxdart: ^0.27.0
stream_chat: ^3.2.0
stream_chat: ^3.3.0
dev_dependencies:
dart_code_metrics: ^4.4.0
@@ -476,7 +476,7 @@ void main() {
_stateSetter?.call(() => limit = 6);
final updatedChannels = _generateChannels(mockClient, count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
final updatedPagination = PaginationParams(limit: limit);
when(() => mockClient.queryChannels(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
@@ -518,7 +518,7 @@ void main() {
_stateSetter?.call(() => limit = 6);
final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
final updatedPagination = PaginationParams(limit: limit);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
@@ -189,9 +189,7 @@ void main() {
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
)).called(
2, // Fetching After messages + Fetching Before messages,
);
)).called(1);
},
);
@@ -214,14 +212,10 @@ void main() {
child: const Offstage(key: childKey),
);
final beforePagination = PaginationParams(
lessThan: initialMessageId,
limit: 20,
);
final afterPagination = PaginationParams(
greaterThanOrEqual: initialMessageId,
limit: 20,
final paginationParams = PaginationParams(
idAround: initialMessageId,
after: 20,
before: 20,
);
when(() => mockChannel.initialized).thenAnswer((_) async => true);
@@ -232,17 +226,7 @@ void main() {
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: beforePagination,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(() => mockChannel.query(
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: afterPagination,
messagesPagination: paginationParams,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
@@ -267,17 +251,7 @@ void main() {
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: beforePagination,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
)).called(1);
verify(() => mockChannel.query(
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: afterPagination,
messagesPagination: paginationParams,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
@@ -285,29 +259,15 @@ void main() {
_stateSetter?.call(() => initialMessageId = 'testInitialMessageId2');
final updatedBeforePagination = beforePagination.copyWith(
lessThan: initialMessageId,
);
final updatedAfterPagination = afterPagination.copyWith(
greaterThanOrEqual: initialMessageId,
final updatedPaginationParams = paginationParams.copyWith(
idAround: initialMessageId,
);
when(() => mockChannel.query(
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: updatedBeforePagination,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(() => mockChannel.query(
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: updatedAfterPagination,
messagesPagination: updatedPaginationParams,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
@@ -319,17 +279,7 @@ void main() {
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: updatedBeforePagination,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
)).called(1);
verify(() => mockChannel.query(
state: any(named: 'state'),
watch: any(named: 'watch'),
presence: any(named: 'presence'),
messagesPagination: updatedAfterPagination,
messagesPagination: updatedPaginationParams,
membersPagination: any(named: 'membersPagination'),
watchersPagination: any(named: 'watchersPagination'),
preferOffline: any(named: 'preferOffline'),
@@ -496,7 +496,7 @@ void main() {
_stateSetter?.call(() => limit = 6);
final updatedUsers = _generateUsers(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
final updatedPagination = PaginationParams(limit: limit);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
@@ -374,8 +374,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
String get youText => 'You';
@override
String galleryPaginationText(
{required int currentPage, required int totalPages}) =>
String galleryPaginationText({
required int currentPage,
required int totalPages,
}) =>
'$currentPage of $totalPages';
@override
@@ -242,7 +242,7 @@ class _MessageViewState extends State<MessageView> {
),
),
),
)
),
],
),
)
@@ -45,7 +45,6 @@ void main() {
role: 'testRole',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: math.Random().nextBool(),
banned: math.Random().nextBool(),
);