Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into feat/capabilities
Conflicts: packages/stream_chat/CHANGELOG.md packages/stream_chat_flutter/CHANGELOG.md
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
|
||||
- name: "Install Tools"
|
||||
run: flutter pub global activate melos 1.0.0-dev.6
|
||||
run: flutter pub global activate melos 1.0.0-dev.10
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ name: stream_flutter_workflow
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||
flutter_version: "2.5.1"
|
||||
melos_version: "1.0.0-dev.10"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -31,7 +32,7 @@ jobs:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: |
|
||||
flutter pub global activate melos 1.0.0-dev.6
|
||||
flutter pub global activate melos ${{ env.melos_version }}
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
- name: "Dart Analyze"
|
||||
@@ -60,7 +61,8 @@ jobs:
|
||||
with:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: flutter pub global activate melos
|
||||
run: |
|
||||
flutter pub global activate melos ${{ env.melos_version }}
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
- name: "Melos Format"
|
||||
@@ -71,7 +73,7 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Git Checkout"
|
||||
uses: actions/checkout@v2
|
||||
@@ -88,7 +90,7 @@ jobs:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: |
|
||||
flutter pub global activate melos
|
||||
flutter pub global activate melos ${{ env.melos_version }}
|
||||
pub global activate remove_from_coverage
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
@@ -120,4 +122,4 @@ jobs:
|
||||
uses: VeryGoodOpenSource/[email protected]
|
||||
with:
|
||||
path: packages/stream_chat_flutter/coverage/lcov.info
|
||||
min_coverage: 67
|
||||
min_coverage: 67
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
We have provided granular control over these actions.
|
||||
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
|
||||
- `ChannelModel` now supplies individual user capabilities.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#799]](https://github.com/GetStream/stream-chat-flutter/issues/799) Fixed `totalUnreadCount` is not updating when
|
||||
app is resumed from background mode
|
||||
|
||||
## 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
|
||||
@@ -726,4 +745,4 @@
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- first beta version
|
||||
- first beta version
|
||||
@@ -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));
|
||||
|
||||
@@ -1466,8 +1466,6 @@ class ChannelClientState {
|
||||
|
||||
_listenMemberRemoved();
|
||||
|
||||
_computeUnread();
|
||||
|
||||
_startCleaning();
|
||||
|
||||
_startCleaningPinnedMessages();
|
||||
@@ -1490,15 +1488,6 @@ class ChannelClientState {
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
void _computeUnread() {
|
||||
final userRead = channelState.read.firstWhereOrNull(
|
||||
(r) => r.user.id == _channel._client.state.currentUser?.id,
|
||||
);
|
||||
if (userRead != null && userRead.unreadMessages > 0) {
|
||||
unreadCount = userRead.unreadMessages;
|
||||
}
|
||||
}
|
||||
|
||||
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
||||
final expiredAttachmentMessagesId = channelState.messages
|
||||
.where((m) =>
|
||||
@@ -1603,7 +1592,7 @@ class ChannelClientState {
|
||||
message.createdAt.isBefore(
|
||||
DateTime.now().subtract(
|
||||
const Duration(
|
||||
seconds: 1,
|
||||
seconds: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1663,7 +1652,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 +1711,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 +1733,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);
|
||||
}
|
||||
}
|
||||
@@ -1843,15 +1839,34 @@ class ChannelClientState {
|
||||
/// Channel read list as a stream.
|
||||
Stream<List<Read>> get readStream => channelStateStream.map((cs) => cs.read);
|
||||
|
||||
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
|
||||
bool _isCurrentUserRead(Read read) =>
|
||||
read.user.id == _channel._client.state.currentUser!.id;
|
||||
|
||||
set unreadCount(int value) => _unreadCountController.add(value);
|
||||
/// Channel read for the logged in user.
|
||||
Read? get currentUserRead => read.firstWhereOrNull(_isCurrentUserRead);
|
||||
|
||||
/// Channel read for the logged in user as a stream.
|
||||
Stream<Read?> get currentUserReadStream =>
|
||||
readStream.map((read) => read.firstWhereOrNull(_isCurrentUserRead));
|
||||
|
||||
/// Unread count getter as a stream.
|
||||
Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
|
||||
Stream<int> get unreadCountStream =>
|
||||
currentUserReadStream.map((read) => read?.unreadMessages ?? 0);
|
||||
|
||||
/// Unread count getter.
|
||||
int get unreadCount => _unreadCountController.value;
|
||||
int get unreadCount => currentUserRead?.unreadMessages ?? 0;
|
||||
|
||||
/// Setter for unread count.
|
||||
set unreadCount(int count) {
|
||||
final reads = [..._channelState.read];
|
||||
final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead);
|
||||
|
||||
if (currentUserReadIndex < 0) return;
|
||||
|
||||
reads[currentUserReadIndex] =
|
||||
reads[currentUserReadIndex].copyWith(unreadMessages: count);
|
||||
_channelState = _channelState.copyWith(read: reads);
|
||||
}
|
||||
|
||||
bool _countMessageAsUnread(Message message) {
|
||||
final userId = _channel.client.state.currentUser?.id;
|
||||
@@ -1860,10 +1875,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.
|
||||
@@ -2108,7 +2126,6 @@ class ChannelClientState {
|
||||
/// Call this method to dispose this object.
|
||||
void dispose() {
|
||||
_debouncedUpdatePersistenceChannelState.cancel();
|
||||
_unreadCountController.close();
|
||||
_retryQueue.dispose();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
_channelStateController.close();
|
||||
|
||||
@@ -395,6 +395,9 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
void _handleHealthCheckEvent(Event event) {
|
||||
final user = event.me;
|
||||
if (user != null) state.currentUser = user;
|
||||
|
||||
final connectionId = event.connectionId;
|
||||
if (connectionId != null) {
|
||||
_connectionIdManager.setConnectionId(connectionId);
|
||||
@@ -1213,8 +1216,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) =>
|
||||
@@ -1432,6 +1441,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -123,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,36 @@
|
||||
|
||||
- `pinPermissions` is no longer needed in `MessageListView`.
|
||||
|
||||
## 3.3.1
|
||||
|
||||
✅ 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
|
||||
|
||||
@@ -866,4 +884,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,
|
||||
|
||||
+9
@@ -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,
|
||||
),
|
||||
|
||||
@@ -204,6 +204,7 @@ class MessageInput extends StatefulWidget {
|
||||
this.commandButtonBuilder,
|
||||
this.customOverlays = const [],
|
||||
this.mentionAllAppUsers = false,
|
||||
this.shouldKeepFocusAfterMessage,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -322,6 +323,10 @@ class MessageInput extends StatefulWidget {
|
||||
/// Defaults to false.
|
||||
final bool mentionAllAppUsers;
|
||||
|
||||
/// 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();
|
||||
|
||||
@@ -1163,7 +1168,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _buildMentionsOverlayEntry() {
|
||||
if (textEditingController.value.selection.start < 0) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (textEditingController.value.selection.start < 0 ||
|
||||
channel.state == null) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
@@ -1192,7 +1199,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) {
|
||||
@@ -1640,7 +1647,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;
|
||||
@@ -1759,7 +1765,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldUnfocus = _commandEnabled;
|
||||
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
|
||||
|
||||
shouldKeepFocus ??= !_commandEnabled;
|
||||
|
||||
if (_commandEnabled) {
|
||||
text = '${'/${_chosenCommand!.name} '}$text';
|
||||
@@ -1822,8 +1830,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 {
|
||||
@@ -170,8 +210,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;
|
||||
|
||||
@@ -285,6 +331,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();
|
||||
}
|
||||
@@ -440,9 +492,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
childAnchor: Alignment.topCenter,
|
||||
message: statusString,
|
||||
child: LazyLoadScrollView(
|
||||
onPageScrollStart: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
@@ -468,6 +517,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
key: (initialIndex != 0 && initialAlignment != 0)
|
||||
? ValueKey('$initialIndex-$initialAlignment')
|
||||
: null,
|
||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
initialScrollIndex: initialIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
@@ -561,17 +611,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) {
|
||||
@@ -1000,15 +1071,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');
|
||||
|
||||
@@ -1137,8 +1199,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;
|
||||
@@ -1223,6 +1225,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 ||
|
||||
@@ -1245,27 +1248,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(
|
||||
|
||||
@@ -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,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.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -36,7 +36,7 @@ dependencies:
|
||||
rxdart: ^0.27.0
|
||||
share_plus: ^3.0.4
|
||||
shimmer: ^2.0.0
|
||||
stream_chat_flutter_core: ^3.2.0
|
||||
stream_chat_flutter_core: ^3.3.0
|
||||
substring_highlight: ^1.0.26
|
||||
synchronized: ^3.0.0
|
||||
url_launcher: ^6.0.3
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 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'),
|
||||
@@ -318,10 +321,10 @@ class _MessageScreenState extends State<MessageScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -157,7 +157,7 @@ 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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'mocks.dart';
|
||||
|
||||
void main() {
|
||||
const pagination = PaginationParams(limit: 3);
|
||||
const pagination = PaginationParams(limit: 3, offset: 0);
|
||||
|
||||
List<Channel> _generateChannels(
|
||||
StreamChatClient client, {
|
||||
@@ -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, offset: 0);
|
||||
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(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user