Merge pull request #936 from GetStream/release/3.5.0

This commit is contained in:
Sahil Kumar
2022-03-02 14:11:14 +05:30
committed by GitHub
28 changed files with 1008 additions and 243 deletions
+20 -2
View File
@@ -1,12 +1,30 @@
## 3.5.0
✅ Added
- You can now pass `score` to `client.sendReaction` and `channel.sendReaction` functions.
- Added new `client.partialUpdateUsers` function in order to partially update users.
🐞 Fixed
- [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890) Fixed Reactions not updating on thread messages.
Thanks [bstolinski](https://github.com/bstolinski).
- [[#897]](https://github.com/GetStream/stream-chat-flutter/issues/897) Fixed error type mis-match in `AuthInterceptor`.
- [[#891]](https://github.com/GetStream/stream-chat-flutter/pull/891) Fixed reply counter for parent message not
updating correctly after deleting thread message.
- Fix `channelState.copyWith` with respect to pinnedMessages.
## 3.4.0 ## 3.4.0
🐞 Fixed 🐞 Fixed
- [[#857]](https://github.com/GetStream/stream-chat-flutter/issues/857) Channel now listens for member ban/unban and - [[#857]](https://github.com/GetStream/stream-chat-flutter/issues/857) Channel now listens for member ban/unban and
updates the channel state with the latest data. updates the channel state with the latest data.
- [[#748]](https://github.com/GetStream/stream-chat-flutter/issues/748) `Message.user` is now also included while saving users in persistence. - [[#748]](https://github.com/GetStream/stream-chat-flutter/issues/748) `Message.user` is now also included while saving
users in persistence.
- [[#871]](https://github.com/GetStream/stream-chat-flutter/issues/871) Fixed thread message deletion. - [[#871]](https://github.com/GetStream/stream-chat-flutter/issues/871) Fixed thread message deletion.
- [[#846]](https://github.com/GetStream/stream-chat-flutter/issues/846) Fixed `message.ownReactions` getting truncated when receiving a reaction event. - [[#846]](https://github.com/GetStream/stream-chat-flutter/issues/846) Fixed `message.ownReactions` getting truncated
when receiving a reaction event.
- Add check for invalid image URLs - Add check for invalid image URLs
- Fix `channelState.pinnedMessagesStream` getting reset to `0` after a channel update. - Fix `channelState.pinnedMessagesStream` getting reset to `0` after a channel update.
- Fixed `unreadCount` after removing user from a channel. - Fixed `unreadCount` after removing user from a channel.
@@ -407,7 +407,7 @@ class Channel {
if (index != -1) { if (index != -1) {
final newAttachments = [...message!.attachments]..[index] = attachment; final newAttachments = [...message!.attachments]..[index] = attachment;
final updatedMessage = message!.copyWith(attachments: newAttachments); final updatedMessage = message!.copyWith(attachments: newAttachments);
state?.addMessage(updatedMessage); state?.updateMessage(updatedMessage);
// updating original message for next iteration // updating original message for next iteration
message = message!.merge(updatedMessage); message = message!.merge(updatedMessage);
} }
@@ -512,7 +512,7 @@ class Channel {
).toList(), ).toList(),
); );
state!.addMessage(message); state!.updateMessage(message);
try { try {
if (message.attachments.any((it) => !it.uploadState.isSuccess)) { if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
@@ -535,7 +535,7 @@ class Channel {
type, type,
skipPush: skipPush, skipPush: skipPush,
); );
state!.addMessage(response.message); state!.updateMessage(response.message);
if (cooldown > 0) cooldownStartedAt = DateTime.now(); if (cooldown > 0) cooldownStartedAt = DateTime.now();
return response; return response;
} catch (e) { } catch (e) {
@@ -571,7 +571,7 @@ class Channel {
).toList(), ).toList(),
); );
state?.addMessage(message); state?.updateMessage(message);
try { try {
if (message.attachments.any((it) => !it.uploadState.isSuccess)) { if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
@@ -594,7 +594,7 @@ class Channel {
ownReactions: message.ownReactions, ownReactions: message.ownReactions,
); );
state?.addMessage(m); state?.updateMessage(m);
return response; return response;
} catch (e) { } catch (e) {
@@ -602,7 +602,7 @@ class Channel {
if (e.isRetriable) { if (e.isRetriable) {
state!._retryQueue.add([message]); state!._retryQueue.add([message]);
} else { } else {
state?.addMessage(originalMessage); state?.updateMessage(originalMessage);
} }
} }
rethrow; rethrow;
@@ -630,7 +630,7 @@ class Channel {
ownReactions: message.ownReactions, ownReactions: message.ownReactions,
); );
state?.addMessage(updatedMessage); state?.updateMessage(updatedMessage);
return response; return response;
} catch (e) { } catch (e) {
@@ -643,13 +643,18 @@ class Channel {
/// Deletes the [message] from the channel. /// Deletes the [message] from the channel.
Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async { Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async {
final hardDelete = hard ?? false;
// Directly deleting the local messages which are not yet sent to server // Directly deleting the local messages which are not yet sent to server
if (message.status == MessageSendingStatus.sending || if (message.status == MessageSendingStatus.sending ||
message.status == MessageSendingStatus.failed) { message.status == MessageSendingStatus.failed) {
state!.addMessage(message.copyWith( state!.deleteMessage(
type: 'deleted', message.copyWith(
status: MessageSendingStatus.sent, type: 'deleted',
)); status: MessageSendingStatus.sent,
),
hardDelete: hardDelete,
);
// Removing the attachments upload completer to stop the `sendMessage` // Removing the attachments upload completer to stop the `sendMessage`
// waiting for attachments to complete. // waiting for attachments to complete.
@@ -667,11 +672,14 @@ class Channel {
deletedAt: message.deletedAt ?? DateTime.now(), deletedAt: message.deletedAt ?? DateTime.now(),
); );
state?.addMessage(message); state?.deleteMessage(message, hardDelete: hardDelete);
final response = await _client.deleteMessage(message.id, hard: hard); final response = await _client.deleteMessage(message.id, hard: hard);
state?.addMessage(message.copyWith(status: MessageSendingStatus.sent)); state?.deleteMessage(
message.copyWith(status: MessageSendingStatus.sent),
hardDelete: hardDelete,
);
return response; return response;
} catch (e) { } catch (e) {
@@ -811,6 +819,7 @@ class Channel {
Future<SendReactionResponse> sendReaction( Future<SendReactionResponse> sendReaction(
Message message, Message message,
String type, { String type, {
int score = 1,
Map<String, Object?> extraData = const {}, Map<String, Object?> extraData = const {},
bool enforceUnique = false, bool enforceUnique = false,
}) async { }) async {
@@ -829,7 +838,7 @@ class Channel {
createdAt: now, createdAt: now,
type: type, type: type,
user: user, user: user,
score: 1, score: score,
extraData: extraData, extraData: extraData,
); );
@@ -860,19 +869,20 @@ class Channel {
ownReactions: ownReactions, ownReactions: ownReactions,
); );
state?.addMessage(newMessage); state?.updateMessage(newMessage);
try { try {
final reactionResp = await _client.sendReaction( final reactionResp = await _client.sendReaction(
messageId, messageId,
type, type,
score: score,
extraData: extraData, extraData: extraData,
enforceUnique: enforceUnique, enforceUnique: enforceUnique,
); );
return reactionResp; return reactionResp;
} catch (_) { } catch (_) {
// Reset the message if the update fails // Reset the message if the update fails
state?.addMessage(message); state?.updateMessage(message);
rethrow; rethrow;
} }
} }
@@ -912,7 +922,7 @@ class Channel {
ownReactions: ownReactions, ownReactions: ownReactions,
); );
state?.addMessage(newMessage); state?.updateMessage(newMessage);
try { try {
final deleteResponse = await _client.deleteReaction( final deleteResponse = await _client.deleteReaction(
@@ -922,7 +932,7 @@ class Channel {
return deleteResponse; return deleteResponse;
} catch (_) { } catch (_) {
// Reset the message if the update fails // Reset the message if the update fails
state?.addMessage(message); state?.updateMessage(message);
rethrow; rethrow;
} }
} }
@@ -1079,7 +1089,7 @@ class Channel {
// update the passed message with response message // update the passed message with response message
if (res.message != null) { if (res.message != null) {
state!.addMessage(res.message!); state!.updateMessage(res.message!);
} else { } else {
// remove the passed message if response does // remove the passed message if response does
// not contain message // not contain message
@@ -1322,7 +1332,8 @@ class Channel {
/// Remove the ban for the user with given [userID] in the channel. /// Remove the ban for the user with given [userID] in the channel.
@Deprecated( @Deprecated(
"Use 'unbanMember' instead. This method will be removed in v4.0.0") "Use 'unbanMember' instead. This method will be removed in v4.0.0",
)
Future<EmptyResponse> unbanUser(String userID) => unbanMember(userID); Future<EmptyResponse> unbanUser(String userID) => unbanMember(userID);
/// Remove the ban for the member with given [userID] in the channel. /// Remove the ban for the member with given [userID] in the channel.
@@ -1694,7 +1705,9 @@ class ChannelClientState {
void _listenReactionDeleted() { void _listenReactionDeleted() {
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
final oldMessage = final oldMessage =
messages.firstWhereOrNull((it) => it.id == event.message?.id); messages.firstWhereOrNull((it) => it.id == event.message?.id) ??
threads[event.message?.parentId]
?.firstWhereOrNull((e) => e.id == event.message?.id);
final reaction = event.reaction; final reaction = event.reaction;
final ownReactions = oldMessage?.ownReactions final ownReactions = oldMessage?.ownReactions
?.whereNot((it) => ?.whereNot((it) =>
@@ -1707,18 +1720,20 @@ class ChannelClientState {
final message = event.message!.copyWith( final message = event.message!.copyWith(
ownReactions: ownReactions, ownReactions: ownReactions,
); );
addMessage(message); updateMessage(message);
})); }));
} }
void _listenReactions() { void _listenReactions() {
_subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { _subscriptions.add(_channel.on(EventType.reactionNew).listen((event) {
final oldMessage = final oldMessage =
messages.firstWhereOrNull((it) => it.id == event.message?.id); messages.firstWhereOrNull((it) => it.id == event.message?.id) ??
threads[event.message?.parentId]
?.firstWhereOrNull((e) => e.id == event.message?.id);
final message = event.message!.copyWith( final message = event.message!.copyWith(
ownReactions: oldMessage?.ownReactions, ownReactions: oldMessage?.ownReactions,
); );
addMessage(message); updateMessage(message);
})); }));
} }
@@ -1730,12 +1745,13 @@ class ChannelClientState {
) )
.listen((event) { .listen((event) {
final oldMessage = final oldMessage =
messages.firstWhereOrNull((it) => it.id == event.message?.id); messages.firstWhereOrNull((it) => it.id == event.message?.id) ??
threads[event.message?.parentId]
?.firstWhereOrNull((e) => e.id == event.message?.id);
final message = event.message!.copyWith( final message = event.message!.copyWith(
ownReactions: oldMessage?.ownReactions, ownReactions: oldMessage?.ownReactions,
); );
addMessage(message); updateMessage(message);
if (message.pinned) { if (message.pinned) {
_channelState = _channelState.copyWith( _channelState = _channelState.copyWith(
@@ -1752,9 +1768,9 @@ class ChannelClientState {
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
final message = event.message!; final message = event.message!;
if (event.hardDelete == true) { if (event.hardDelete == true) {
removeMessage(message, hardDelete: true); removeMessage(message);
} else { } else {
addMessage(message); updateMessage(message);
} }
})); }));
} }
@@ -1769,7 +1785,7 @@ class ChannelClientState {
final message = event.message!; final message = event.message!;
if (isUpToDate || if (isUpToDate ||
(message.parentId != null && message.showInChannel != true)) { (message.parentId != null && message.showInChannel != true)) {
addMessage(message); updateMessage(message);
} }
if (_countMessageAsUnread(message)) { if (_countMessageAsUnread(message)) {
@@ -1779,9 +1795,13 @@ class ChannelClientState {
} }
/// Add a [message] to this [channelState]. /// Add a [message] to this [channelState].
void addMessage(Message message) { @Deprecated('Use updateMessage instead')
void addMessage(Message message) => updateMessage(message);
/// Updates the [message] in the state if it exists. Adds it otherwise.
void updateMessage(Message message) {
if (message.parentId == null || message.showInChannel == true) { if (message.parentId == null || message.showInChannel == true) {
final newMessages = List<Message>.from(_channelState.messages); final newMessages = [...messages];
final oldIndex = newMessages.indexWhere((m) => m.id == message.id); final oldIndex = newMessages.indexWhere((m) => m.id == message.id);
if (oldIndex != -1) { if (oldIndex != -1) {
Message? m; Message? m;
@@ -1796,8 +1816,24 @@ class ChannelClientState {
newMessages.add(message); newMessages.add(message);
} }
final newPinnedMessages = [...pinnedMessages];
final oldPinnedIndex =
newPinnedMessages.indexWhere((m) => m.id == message.id);
// Handle pinned messages
if (message.pinned) {
if (oldPinnedIndex != -1) {
newPinnedMessages[oldPinnedIndex] = message;
} else {
newPinnedMessages.add(message);
}
} else {
newPinnedMessages.removeWhere((m) => m.id == message.id);
}
_channelState = _channelState.copyWith( _channelState = _channelState.copyWith(
messages: newMessages..sort(_sortByCreatedAt), messages: newMessages..sort(_sortByCreatedAt),
pinnedMessages: newPinnedMessages,
channel: _channelState.channel?.copyWith( channel: _channelState.channel?.copyWith(
lastMessageAt: message.createdAt, lastMessageAt: message.createdAt,
), ),
@@ -1810,41 +1846,35 @@ class ChannelClientState {
} }
/// Remove a [message] from this [channelState]. /// Remove a [message] from this [channelState].
void removeMessage(Message message, {bool hardDelete = false}) { void removeMessage(Message message) {
final parentId = message.parentId; final parentId = message.parentId;
// i.e. it's a thread message // i.e. it's a thread message, Remove it
// 1. Remove the thread message
// 2. Reduce total reply count of parent message
if (parentId != null) { if (parentId != null) {
final allMessages = [...messages]; final newThreads = {...threads};
final parentMessage = allMessages.firstWhereOrNull( // Early return in case the thread is not available
(it) => it.id == parentId, if (!newThreads.containsKey(parentId)) return;
);
// return if message not available in the memory _threads = newThreads
if (parentMessage == null) return; ..update(
final replyCount = parentMessage.replyCount; parentId,
// return if reply count is null or zero (messages) => messages..removeWhere((e) => e.id == message.id),
if (replyCount == null || replyCount == 0) return; );
addMessage(parentMessage.copyWith(replyCount: replyCount - 1)); // Early return if the thread message is not shown in channel.
updateThreadInfo( if (message.showInChannel == false) return;
parentId,
threads[parentId]!
..removeWhere(
(e) => e.id == message.id,
),
);
} else {
// Remove regular message
final allMessages = [...messages];
if (hardDelete) {
allMessages.removeWhere((e) => e.id == message.id);
_channelState = _channelState.copyWith(messages: allMessages);
} else if (allMessages.remove(message)) {
_channelState = _channelState.copyWith(messages: allMessages);
}
} }
// Remove regular message, thread message shown in channel
final allMessages = [...messages];
_channelState = _channelState.copyWith(
messages: allMessages..removeWhere((e) => e.id == message.id),
);
}
/// Removes/Updates the [message] based on the [hardDelete] value.
void deleteMessage(Message message, {bool hardDelete = false}) {
if (hardDelete) return removeMessage(message);
return updateMessage(message);
} }
void _listenReadEvents() { void _listenReadEvents() {
@@ -1890,11 +1920,12 @@ class ChannelClientState {
.distinct(const ListEquality().equals); .distinct(const ListEquality().equals);
/// Channel pinned message list. /// Channel pinned message list.
List<Message> get pinnedMessages => _channelState.pinnedMessages.toList(); List<Message> get pinnedMessages => _channelState.pinnedMessages;
/// Channel pinned message list as a stream. /// Channel pinned message list as a stream.
Stream<List<Message>> get pinnedMessagesStream => Stream<List<Message>> get pinnedMessagesStream => channelStateStream
channelStateStream.map((cs) => cs.pinnedMessages.toList()); .map((cs) => cs.pinnedMessages)
.distinct(const ListEquality().equals);
/// Get channel last message. /// Get channel last message.
Message? get lastMessage => Message? get lastMessage =>
@@ -2205,7 +2236,7 @@ class ChannelClientState {
.toList(); .toList();
updateChannelState(_channelState.copyWith( updateChannelState(_channelState.copyWith(
pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(), pinnedMessages: pinnedMessages.where(_pinIsValid).toList(),
messages: expiredMessages, messages: expiredMessages,
)); ));
} }
@@ -2242,7 +2273,7 @@ class ChannelClientState {
} }
} }
bool Function(Message) _pinIsValid() { bool _pinIsValid(Message message) {
final now = DateTime.now(); final now = DateTime.now();
return (Message m) => m.pinExpires!.isAfter(now); return message.pinExpires!.isAfter(now);
} }
@@ -1073,6 +1073,28 @@ class StreamChatClient {
Future<UpdateUsersResponse> updateUsers(List<User> users) => Future<UpdateUsersResponse> updateUsers(List<User> users) =>
_chatApi.user.updateUsers(users); _chatApi.user.updateUsers(users);
/// Partially update the given user with [id].
/// Use [set] to define values to be set.
/// Use [unset] to define values to be unset.
Future<UpdateUsersResponse> partialUpdateUser(
String id, {
Map<String, Object?>? set,
List<String>? unset,
}) {
final user = PartialUpdateUserRequest(
id: id,
set: set,
unset: unset,
);
return partialUpdateUsers([user]);
}
/// Batch partial updates the [users].
Future<UpdateUsersResponse> partialUpdateUsers(
List<PartialUpdateUserRequest> users,
) =>
_chatApi.user.partialUpdateUsers(users);
/// Bans a user from all channels /// Bans a user from all channels
Future<EmptyResponse> banUser( Future<EmptyResponse> banUser(
String targetUserId, [ String targetUserId, [
@@ -1157,15 +1179,22 @@ class StreamChatClient {
Future<SendReactionResponse> sendReaction( Future<SendReactionResponse> sendReaction(
String messageId, String messageId,
String reactionType, { String reactionType, {
int score = 1,
Map<String, Object?> extraData = const {}, Map<String, Object?> extraData = const {},
bool enforceUnique = false, bool enforceUnique = false,
}) => }) {
_chatApi.message.sendReaction( final _extraData = {
messageId, 'score': score,
reactionType, ...extraData,
extraData: extraData, };
enforceUnique: enforceUnique,
); return _chatApi.message.sendReaction(
messageId,
reactionType,
extraData: _extraData,
enforceUnique: enforceUnique,
);
}
/// Delete a [reactionType] from this [messageId] /// Delete a [reactionType] from this [messageId]
Future<EmptyResponse> deleteReaction( Future<EmptyResponse> deleteReaction(
@@ -158,7 +158,7 @@ class RetryQueue {
: message.status == MessageSendingStatus.updating : message.status == MessageSendingStatus.updating
? MessageSendingStatus.failed_update ? MessageSendingStatus.failed_update
: MessageSendingStatus.failed_delete; : MessageSendingStatus.failed_delete;
channel.state?.addMessage(message.copyWith(status: newStatus)); channel.state?.updateMessage(message.copyWith(status: newStatus));
} }
Future<void> _retryMessage(Message message) async { Future<void> _retryMessage(Message message) async {
@@ -156,3 +156,29 @@ class PaginationParams extends Equatable {
lessThanOrEqual, lessThanOrEqual,
]; ];
} }
/// Request model for the [client.partialUpdateUser] api call.
@JsonSerializable(createFactory: false)
class PartialUpdateUserRequest extends Equatable {
/// Creates a new PartialUpdateUserRequest instance.
const PartialUpdateUserRequest({
required this.id,
this.set,
this.unset,
});
/// User ID.
final String id;
/// Fields to set.
final Map<String, Object?>? set;
/// Fields to unset.
final List<String>? unset;
/// Serialize model to json
Map<String, dynamic> toJson() => _$PartialUpdateUserRequestToJson(this);
@override
List<Object?> get props => [id, set, unset];
}
@@ -54,3 +54,14 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
writeNotNull('id_lte', instance.lessThanOrEqual); writeNotNull('id_lte', instance.lessThanOrEqual);
return val; return val;
} }
Map<String, dynamic> _$PartialUpdateUserRequestToJson(
PartialUpdateUserRequest instance) =>
<String, dynamic>{
'stringify': instance.stringify,
'hash_code': instance.hashCode,
'id': instance.id,
'set': instance.set,
'unset': instance.unset,
'props': instance.props,
};
@@ -46,4 +46,17 @@ class UserApi {
); );
return UpdateUsersResponse.fromJson(response.data); return UpdateUsersResponse.fromJson(response.data);
} }
/// Batch partial update of [users].
Future<UpdateUsersResponse> partialUpdateUsers(
List<PartialUpdateUserRequest> users,
) async {
final response = await _client.patch(
'/users',
data: {
'users': users,
},
);
return UpdateUsersResponse.fromJson(response.data);
}
} }
@@ -49,10 +49,13 @@ class AuthInterceptor extends Interceptor {
DioError err, DioError err,
ErrorInterceptorHandler handler, ErrorInterceptorHandler handler,
) async { ) async {
ErrorResponse? error;
final data = err.response?.data; final data = err.response?.data;
if (data != null) error = ErrorResponse.fromJson(data); if (data == null || data is! Map<String, dynamic>) {
if (error?.code == ChatErrorCode.tokenExpired.code) { return handler.next(err);
}
final error = ErrorResponse.fromJson(data);
if (error.code == ChatErrorCode.tokenExpired.code) {
if (_tokenManager.isStatic) return handler.next(err); if (_tokenManager.isStatic) return handler.next(err);
_client.lock(); _client.lock();
await _tokenManager.loadToken(refresh: true); await _tokenManager.loadToken(refresh: true);
@@ -56,7 +56,7 @@ class ChannelState {
ChannelModel? channel, ChannelModel? channel,
List<Message>? messages, List<Message>? messages,
List<Member>? members, List<Member>? members,
List<Message>? pinnedMessages, List<Message> pinnedMessages = _emptyPinnedMessages,
int? watcherCount, int? watcherCount,
List<User>? watchers, List<User>? watchers,
List<Read>? read, List<Read>? read,
@@ -69,7 +69,7 @@ class ChannelState {
// FIXME: Use non-nullable by default instead of empty list. // FIXME: Use non-nullable by default instead of empty list.
pinnedMessages: pinnedMessages == _emptyPinnedMessages pinnedMessages: pinnedMessages == _emptyPinnedMessages
? this.pinnedMessages ? this.pinnedMessages
: pinnedMessages ?? _emptyPinnedMessages, : pinnedMessages,
watcherCount: watcherCount ?? this.watcherCount, watcherCount: watcherCount ?? this.watcherCount,
watchers: watchers ?? this.watchers, watchers: watchers ?? this.watchers,
read: read ?? this.read, read: read ?? this.read,
@@ -21,7 +21,7 @@ ChannelState _$ChannelStateFromJson(Map<String, dynamic> json) => ChannelState(
pinnedMessages: (json['pinned_messages'] as List<dynamic>?) pinnedMessages: (json['pinned_messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], _emptyPinnedMessages,
watcherCount: json['watcher_count'] as int?, watcherCount: json['watcher_count'] as int?,
watchers: (json['watchers'] as List<dynamic>?) watchers: (json['watchers'] as List<dynamic>?)
?.map((e) => User.fromJson(e as Map<String, dynamic>)) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '3.4.0'; const PACKAGE_VERSION = '3.5.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. description: The official Dart client for Stream Chat, a service for building chat applications.
version: 3.4.0 version: 3.5.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -1116,6 +1116,135 @@ void main() {
verify(() => client.sendReaction(message.id, type)).called(1); verify(() => client.sendReaction(message.id, type)).called(1);
}); });
test('should work fine with score passed explicitly', () async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
const score = 5;
final reaction = Reaction(
type: type,
messageId: message.id,
score: score,
);
when(() => client.sendReaction(
message.id,
type,
score: score,
)).thenAnswer(
(_) async => SendReactionResponse()
..message = message
..reaction = reaction,
);
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
reactionCounts: {type: 1},
reactionScores: {type: score},
latestReactions: [reaction],
ownReactions: [reaction],
),
matchReactions: true,
matchSendingStatus: true,
),
],
]),
);
final res = await channel.sendReaction(
message,
type,
score: score,
);
expect(res, isNotNull);
expect(res.reaction.type, type);
expect(res.reaction.messageId, message.id);
expect(res.reaction.score, score);
verify(() => client.sendReaction(
message.id,
type,
score: score,
)).called(1);
});
test('should work fine with score passed explicitly and in extraData',
() async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
const score = 5;
const extraDataScore = 3;
const extraData = {
'score': extraDataScore,
};
final reaction = Reaction(
type: type,
messageId: message.id,
score: extraDataScore,
);
when(() => client.sendReaction(
message.id,
type,
score: score,
extraData: extraData,
)).thenAnswer(
(_) async => SendReactionResponse()
..message = message
..reaction = reaction,
);
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
reactionCounts: {type: 1},
reactionScores: {type: extraDataScore},
latestReactions: [reaction],
ownReactions: [reaction],
),
matchReactions: true,
matchSendingStatus: true,
),
],
]),
);
final res = await channel.sendReaction(
message,
type,
score: score,
extraData: extraData,
);
expect(res, isNotNull);
expect(res.reaction.type, type);
expect(res.reaction.messageId, message.id);
expect(
res.reaction.score,
extraDataScore,
);
verify(() => client.sendReaction(
message.id,
type,
score: score,
extraData: extraData,
)).called(1);
});
test( test(
'should restore previous message if `client.sendReaction` throws', 'should restore previous message if `client.sendReaction` throws',
() async { () async {
@@ -1239,6 +1368,189 @@ void main() {
); );
}); });
group('`.sendReaction in thread`', () {
test('should work fine', () async {
const type = 'test-reaction-type';
final message = Message(
id: 'test-message-id',
parentId: 'test-parent-id', // is thread message
);
final reaction = Reaction(type: type, messageId: message.id);
when(() => client.sendReaction(message.id, type)).thenAnswer(
(_) async => SendReactionResponse()
..message = message
..reaction = reaction,
);
expectLater(
channel.state?.threadsStream
// skipping first seed message list -> [] messages
.skip(1)
.map((event) => event['test-parent-id']),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
reactionCounts: {type: 1},
reactionScores: {type: 1},
latestReactions: [reaction],
ownReactions: [reaction],
),
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
]),
);
final res = await channel.sendReaction(message, type);
expect(res, isNotNull);
expect(res.reaction.type, type);
expect(res.reaction.messageId, message.id);
verify(() => client.sendReaction(message.id, type)).called(1);
});
test(
'''should restore previous thread message if `client.sendReaction` throws''',
() async {
const type = 'test-reaction-type';
final message = Message(
id: 'test-message-id',
parentId: 'test-parent-id', // is thread message
);
final reaction = Reaction(type: type, messageId: message.id);
when(() => client.sendReaction(message.id, type))
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.threadsStream
.skip(1)
.map((event) => event['test-parent-id']),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
reactionCounts: {type: 1},
reactionScores: {type: 1},
latestReactions: [reaction],
ownReactions: [reaction],
),
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
[
isSameMessageAs(
message,
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
]),
);
try {
await channel.sendReaction(message, type);
} catch (e) {
expect(e, isA<StreamChatNetworkError>());
}
verify(() => client.sendReaction(message.id, type)).called(1);
},
);
test(
'''should override previous thread reaction if present and `enforceUnique` is true''',
() async {
const userId = 'test-user-id';
const messageId = 'test-message-id';
const parentId = 'test-parent-id';
const prevType = 'test-reaction-type';
final prevReaction = Reaction(
type: prevType,
messageId: messageId,
userId: userId,
);
final message = Message(
id: messageId,
parentId: parentId,
ownReactions: [prevReaction],
latestReactions: [prevReaction],
reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1},
);
const type = 'test-reaction-type-2';
final newReaction = Reaction(
type: type,
messageId: messageId,
userId: userId,
);
final newMessage = message.copyWith(
ownReactions: [newReaction],
latestReactions: [newReaction],
);
const enforceUnique = true;
when(() => client.sendReaction(
messageId,
type,
enforceUnique: enforceUnique,
)).thenAnswer(
(_) async => SendReactionResponse()
..message = newMessage
..reaction = newReaction,
);
expectLater(
// skipping first seed message list -> [] messages
channel.state?.threadsStream
.skip(1)
.map((event) => event['test-parent-id']),
emitsInOrder([
[
isSameMessageAs(
newMessage.copyWith(status: MessageSendingStatus.sent),
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
]),
);
final res = await channel.sendReaction(
message,
type,
enforceUnique: enforceUnique,
);
expect(res, isNotNull);
expect(res.reaction.type, type);
expect(res.reaction.messageId, messageId);
verify(() => client.sendReaction(
messageId,
type,
enforceUnique: enforceUnique,
)).called(1);
},
);
});
group('`.deleteReaction`', () { group('`.deleteReaction`', () {
test('should work fine', () async { test('should work fine', () async {
const userId = 'test-user-id'; const userId = 'test-user-id';
@@ -1343,6 +1655,121 @@ void main() {
); );
}); });
group('`.deleteReaction in thread`', () {
test('should work fine', () async {
const userId = 'test-user-id';
const messageId = 'test-message-id';
const parentId = 'test-parent-id';
const type = 'test-reaction-type';
final reaction = Reaction(
type: type,
messageId: messageId,
userId: userId,
);
final message = Message(
id: messageId,
parentId: parentId, // is thread
ownReactions: [reaction],
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
);
when(() => client.deleteReaction(messageId, type))
.thenAnswer((_) async => EmptyResponse());
expectLater(
// skipping first seed message list -> [] messages
channel.state?.threadsStream
.skip(1)
.map((event) => event['test-parent-id']),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
latestReactions: [],
ownReactions: [],
),
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
]),
);
final res = await channel.deleteReaction(message, reaction);
expect(res, isNotNull);
verify(() => client.deleteReaction(messageId, type)).called(1);
});
test(
'should restore prev message state if `client.deleteReaction` throws',
() async {
const userId = 'test-user-id';
const messageId = 'test-message-id';
const parentId = 'test-parent-id';
const type = 'test-reaction-type';
final reaction = Reaction(
type: type,
messageId: messageId,
userId: userId,
);
final message = Message(
id: messageId,
parentId: parentId,
ownReactions: [reaction],
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
);
when(() => client.deleteReaction(messageId, type))
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.threadsStream
.skip(1)
.map((event) => event['test-parent-id']),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
latestReactions: [],
ownReactions: [],
),
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
[
isSameMessageAs(
message,
matchReactions: true,
matchSendingStatus: true,
matchParentId: true,
),
],
]),
);
try {
await channel.deleteReaction(message, reaction);
} catch (e) {
expect(e, isA<StreamChatNetworkError>());
}
verify(() => client.deleteReaction(messageId, type)).called(1);
},
);
});
test('`.update`', () async { test('`.update`', () async {
const channelData = { const channelData = {
'name': 'Stream Team', 'name': 'Stream Team',
@@ -1772,6 +1772,46 @@ void main() {
verifyNoMoreInteractions(api.user); verifyNoMoreInteractions(api.user);
}); });
test('`.partialUpdateUser`', () async {
const userId = 'test-user-id';
final set = {'color': 'yellow'};
final unset = <String>[];
final partialUpdateRequest = PartialUpdateUserRequest(
id: userId,
set: set,
unset: unset,
);
final updatedUser = User(
id: userId,
extraData: {'color': set['color']},
);
when(() => api.user.partialUpdateUsers([partialUpdateRequest]))
.thenAnswer(
(_) async => UpdateUsersResponse()
..users = {
updatedUser.id: updatedUser,
},
);
final res = await client.partialUpdateUser(
userId,
set: set,
unset: unset,
);
expect(res, isNotNull);
expect(res.users, {updatedUser.id: updatedUser});
verify(
() => api.user.partialUpdateUsers([partialUpdateRequest]),
).called(1);
verifyNoMoreInteractions(api.user);
});
test('`.banUser`', () async { test('`.banUser`', () async {
const userId = 'test-user-id'; const userId = 'test-user-id';
@@ -1956,23 +1996,109 @@ void main() {
verifyNoMoreInteractions(api.channel); verifyNoMoreInteractions(api.channel);
}); });
test('`.sendReaction`', () async { group('`.sendReaction`', () {
const messageId = 'test-message-id'; test('`.sendReaction with default params`', () async {
const reactionType = 'like'; const messageId = 'test-message-id';
const reactionType = 'like';
const extraData = {'score': 1};
when(() => api.message.sendReaction(messageId, reactionType)) when(() => api.message.sendReaction(
.thenAnswer((_) async => SendReactionResponse() messageId,
..message = Message(id: messageId) reactionType,
..reaction = Reaction(type: reactionType, messageId: messageId)); extraData: extraData,
)).thenAnswer((_) async => SendReactionResponse()
..message = Message(id: messageId)
..reaction = Reaction(type: reactionType, messageId: messageId));
final res = await client.sendReaction(messageId, reactionType); final res = await client.sendReaction(messageId, reactionType);
expect(res, isNotNull); expect(res, isNotNull);
expect(res.message.id, messageId); expect(res.message.id, messageId);
expect(res.reaction.type, reactionType); expect(res.reaction.type, reactionType);
expect(res.reaction.messageId, messageId); expect(res.reaction.messageId, messageId);
verify(() => api.message.sendReaction(messageId, reactionType)).called(1); verify(() => api.message.sendReaction(
verifyNoMoreInteractions(api.message); messageId,
reactionType,
extraData: extraData,
)).called(1);
verifyNoMoreInteractions(api.message);
});
test('`.sendReaction with score`', () async {
const messageId = 'test-message-id';
const reactionType = 'like';
const score = 3;
const extraData = {'score': score};
when(() => api.message.sendReaction(
messageId,
reactionType,
extraData: extraData,
)).thenAnswer((_) async => SendReactionResponse()
..message = Message(id: messageId)
..reaction = Reaction(
type: reactionType,
messageId: messageId,
score: score,
));
final res = await client.sendReaction(
messageId,
reactionType,
score: score,
);
expect(res, isNotNull);
expect(res.message.id, messageId);
expect(res.reaction.type, reactionType);
expect(res.reaction.messageId, messageId);
expect(res.reaction.score, score);
verify(() => api.message.sendReaction(
messageId,
reactionType,
extraData: extraData,
)).called(1);
verifyNoMoreInteractions(api.message);
});
test('`.sendReaction with score passed in extradata also`', () async {
const messageId = 'test-message-id';
const reactionType = 'like';
const score = 3;
const extraDataScore = 5;
const extraData = {'score': extraDataScore};
when(() => api.message.sendReaction(
messageId,
reactionType,
extraData: extraData,
)).thenAnswer((_) async => SendReactionResponse()
..message = Message(id: messageId)
..reaction = Reaction(
type: reactionType,
messageId: messageId,
score: extraDataScore,
));
final res = await client.sendReaction(
messageId,
reactionType,
score: score,
extraData: extraData,
);
expect(res, isNotNull);
expect(res.message.id, messageId);
expect(res.reaction.type, reactionType);
expect(res.reaction.messageId, messageId);
expect(res.reaction.score, extraDataScore);
verify(() => api.message.sendReaction(
messageId,
reactionType,
extraData: extraData,
)).called(1);
verifyNoMoreInteractions(api.message);
});
}); });
test('`.deleteReaction`', () async { test('`.deleteReaction`', () async {
@@ -175,14 +175,14 @@ void main() {
test('markAllRead', () async { test('markAllRead', () async {
const path = '/channels/read'; const path = '/channels/read';
when(() => client.post(path)).thenAnswer( when(() => client.post(path, data: {})).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{})); (_) async => successResponse(path, data: <String, dynamic>{}));
final res = await channelApi.markAllRead(); final res = await channelApi.markAllRead();
expect(res, isNotNull); expect(res, isNotNull);
verify(() => client.post(path)).called(1); verify(() => client.post(path, data: {})).called(1);
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
@@ -82,4 +82,37 @@ void main() {
verify(() => client.post(path, data: any(named: 'data'))).called(1); verify(() => client.post(path, data: any(named: 'data'))).called(1);
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
test('partialUpdateUsers', () async {
const user = PartialUpdateUserRequest(
id: 'test-user-id',
set: {'color': 'yellow'},
);
const path = '/users';
final updatedUser = {user.id: User(id: user.id, extraData: user.set!)};
when(() => client.patch(path, data: {
'users': [user],
})).thenAnswer(
(_) async => successResponse(
path,
data: {
'users':
updatedUser.map((key, value) => MapEntry(key, value.toJson()))
},
),
);
final res = await userApi.partialUpdateUsers([user]);
expect(res, isNotNull);
expect(res.users.length, updatedUser.length);
verify(() => client.patch(path, data: {
'users': [user]
})).called(1);
verifyNoMoreInteractions(client);
});
} }
@@ -49,6 +49,7 @@ Matcher isSameMessageAs(
bool matchSendingStatus = false, bool matchSendingStatus = false,
bool matchAttachments = false, bool matchAttachments = false,
bool matchAttachmentsUploadState = false, bool matchAttachmentsUploadState = false,
bool matchParentId = false,
}) => }) =>
_IsSameMessageAs( _IsSameMessageAs(
targetMessage: targetMessage, targetMessage: targetMessage,
@@ -57,6 +58,7 @@ Matcher isSameMessageAs(
matchSendingStatus: matchSendingStatus, matchSendingStatus: matchSendingStatus,
matchAttachments: matchAttachments, matchAttachments: matchAttachments,
matchAttachmentsUploadState: matchAttachmentsUploadState, matchAttachmentsUploadState: matchAttachmentsUploadState,
matchParentId: matchParentId,
); );
class _IsSameMessageAs extends Matcher { class _IsSameMessageAs extends Matcher {
@@ -67,6 +69,7 @@ class _IsSameMessageAs extends Matcher {
this.matchSendingStatus = false, this.matchSendingStatus = false,
this.matchAttachments = false, this.matchAttachments = false,
this.matchAttachmentsUploadState = false, this.matchAttachmentsUploadState = false,
this.matchParentId = false,
}); });
final Message targetMessage; final Message targetMessage;
@@ -75,6 +78,7 @@ class _IsSameMessageAs extends Matcher {
final bool matchSendingStatus; final bool matchSendingStatus;
final bool matchAttachments; final bool matchAttachments;
final bool matchAttachmentsUploadState; final bool matchAttachmentsUploadState;
final bool matchParentId;
@override @override
Description describe(Description description) => Description describe(Description description) =>
@@ -123,6 +127,9 @@ class _IsSameMessageAs extends Matcher {
matches &= matchAttachments(); matches &= matchAttachments();
} }
if (matchParentId) {
matches &= message.parentId == targetMessage.parentId;
}
return matches; return matches;
} }
} }
@@ -1,3 +1,12 @@
## 3.5.0
🐞 Fixed
- [[#888]](https://github.com/GetStream/stream-chat-flutter/issues/888) Fix `unban` command not working in `MessageInput`.
- [[#805]](https://github.com/GetStream/stream-chat-flutter/issues/805) Updated chewie dependency version to 1.3.0
- Fix `showScrollToBottom` in `MessageListView` not respecting false value.
- Fix default `Channel` route not opening from `ChannelListView` when `ChannelAvatar` is tapped
## 3.4.0 ## 3.4.0
- Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). - Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 46; objectVersion = 50;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1020; LastUpgradeCheck = 1300;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1300"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -599,7 +599,9 @@ class _ChannelListViewState extends State<ChannelListView> {
child: ChannelPreview( child: ChannelPreview(
onLongPress: widget.onChannelLongPress, onLongPress: widget.onChannelLongPress,
channel: channel, channel: channel,
onImageTap: () => widget.onImageTap?.call(channel), onImageTap: widget.onImageTap != null
? () => widget.onImageTap!(channel)
: null,
onTap: (channel) => onTap(channel, widget.channelWidget), onTap: (channel) => onTap(channel, widget.channelWidget),
), ),
), ),
@@ -612,7 +614,7 @@ class _ChannelListViewState extends State<ChannelListView> {
if (widget.onChannelTap != null) { if (widget.onChannelTap != null) {
onTap = widget.onChannelTap!; onTap = widget.onChannelTap!;
} else { } else {
onTap = (client, _) { onTap = (channel, _) {
if (widget.channelWidget == null) { if (widget.channelWidget == null) {
return; return;
} }
@@ -620,7 +622,7 @@ class _ChannelListViewState extends State<ChannelListView> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: client, channel: channel,
child: widget.channelWidget!, child: widget.channelWidget!,
), ),
), ),
@@ -65,24 +65,35 @@ class FullScreenMedia extends StatefulWidget {
class _FullScreenMediaState extends State<FullScreenMedia> class _FullScreenMediaState extends State<FullScreenMedia>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
bool _optionsShown = true; late final AnimationController _animationController;
late final AnimationController _controller;
late final PageController _pageController; late final PageController _pageController;
late int _currentPage; late final _curvedAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
final _opacityTween = Tween<double>(begin: 1, end: 0);
late final _opacityAnimation = _opacityTween.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.6, curve: Curves.easeOut),
),
);
late final ValueNotifier<int> _currentPage = ValueNotifier(widget.startIndex);
final videoPackages = <String, VideoPackage>{}; final videoPackages = <String, VideoPackage>{};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
); );
_pageController = PageController(initialPage: widget.startIndex); _pageController = PageController(initialPage: widget.startIndex);
_currentPage = widget.startIndex;
for (var i = 0; i < widget.mediaAttachments.length; i++) { for (var i = 0; i < widget.mediaAttachments.length; i++) {
final attachment = widget.mediaAttachments[i]; final attachment = widget.mediaAttachments[i];
if (attachment.type != 'video') continue; if (attachment.type != 'video') continue;
@@ -116,41 +127,38 @@ class _FullScreenMediaState extends State<FullScreenMedia>
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
children: [ children: [
AnimatedBuilder( PageView.builder(
animation: _controller, controller: _pageController,
builder: (context, snapshot) => PageView.builder( onPageChanged: (val) {
controller: _pageController, _currentPage.value = val;
onPageChanged: (val) {
setState(() {
_currentPage = val;
});
if (videoPackages.isEmpty) { if (videoPackages.isEmpty) {
return; return;
}
final currentAttachment = widget.mediaAttachments[val];
for (final e in videoPackages.values) {
if (e._attachment != currentAttachment) {
e._chewieController?.pause();
} }
}
final currentAttachment = widget.mediaAttachments[val]; if (widget.autoplayVideos &&
currentAttachment.type == 'video') {
for (final e in videoPackages.values) { final controller = videoPackages[currentAttachment.id]!;
if (e._attachment != currentAttachment) { controller._chewieController?.play();
e._chewieController?.pause(); }
} },
} itemBuilder: (context, index) {
final attachment = widget.mediaAttachments[index];
if (widget.autoplayVideos && if (attachment.type == 'image' || attachment.type == 'giphy') {
currentAttachment.type == 'video') { final imageUrl = attachment.imageUrl ??
final controller = videoPackages[currentAttachment.id]!; attachment.assetUrl ??
controller._chewieController?.play(); attachment.thumbUrl;
} return AnimatedBuilder(
}, animation: _curvedAnimation,
itemBuilder: (context, index) { builder: (context, child) => PhotoView(
final attachment = widget.mediaAttachments[index];
if (attachment.type == 'image' ||
attachment.type == 'giphy') {
final imageUrl = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
return PhotoView(
loadingBuilder: (context, image) => const Offstage(), loadingBuilder: (context, image) => const Offstage(),
imageProvider: (imageUrl == null && imageProvider: (imageUrl == null &&
attachment.localUri != null && attachment.localUri != null &&
@@ -166,97 +174,91 @@ class _FullScreenMediaState extends State<FullScreenMedia>
color: ColorTween( color: ColorTween(
begin: ChannelHeaderTheme.of(context).color, begin: ChannelHeaderTheme.of(context).color,
end: Colors.black, end: Colors.black,
).lerp(_controller.value), ).lerp(_curvedAnimation.value),
), ),
onTapUp: (a, b, c) { onTapUp: (a, b, c) {
setState(() { if (_animationController.isCompleted) {
_optionsShown = !_optionsShown; _animationController.reverse();
});
if (_controller.isCompleted) {
_controller.reverse();
} else { } else {
_controller.forward(); _animationController.forward();
} }
}, },
); ),
} else if (attachment.type == 'video') { );
final controller = videoPackages[attachment.id]!; } else if (attachment.type == 'video') {
if (!controller.initialized) { final controller = videoPackages[attachment.id]!;
return const Center( if (!controller.initialized) {
child: CircularProgressIndicator(), return const Center(
); child: CircularProgressIndicator(),
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50,
),
child: Chewie(
controller: controller.chewieController!,
),
),
); );
} }
return Container(); return InkWell(
}, onTap: () {
itemCount: widget.mediaAttachments.length, if (_animationController.isCompleted) {
), _animationController.reverse();
), } else {
AnimatedOpacity( _animationController.forward();
opacity: _optionsShown ? 1.0 : 0.0, }
duration: const Duration(milliseconds: 300), },
child: Column( child: Padding(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.symmetric(
children: [ vertical: 50,
GalleryHeader( ),
userName: widget.userName, child: Chewie(
sentAt: context.translations.sentAtText( controller: controller.chewieController!,
date: widget.message.createdAt, ),
time: widget.message.createdAt,
), ),
onBackPressed: () { );
Navigator.of(context).pop(); }
}, return const SizedBox();
message: widget.message, },
currentIndex: _currentPage, itemCount: widget.mediaAttachments.length,
onShowMessage: () { ),
widget.onShowMessage?.call( FadeTransition(
widget.message, opacity: _opacityAnimation,
StreamChannel.of(context).channel, child: ValueListenableBuilder<int>(
); valueListenable: _currentPage,
}, builder: (context, value, child) => Column(
attachmentActionsModalBuilder: mainAxisAlignment: MainAxisAlignment.spaceBetween,
widget.attachmentActionsModalBuilder, children: [
), GalleryHeader(
if (!widget.message.isEphemeral) userName: widget.userName,
GalleryFooter( sentAt: context.translations.sentAtText(
currentPage: _currentPage, date: widget.message.createdAt,
totalPages: widget.mediaAttachments.length, time: widget.message.createdAt,
mediaAttachments: widget.mediaAttachments, ),
onBackPressed: () {
Navigator.of(context).pop();
},
message: widget.message, message: widget.message,
mediaSelectedCallBack: (val) { currentIndex: value,
setState(() { onShowMessage: () {
_currentPage = val; widget.onShowMessage?.call(
widget.message,
StreamChannel.of(context).channel,
);
},
attachmentActionsModalBuilder:
widget.attachmentActionsModalBuilder,
),
if (!widget.message.isEphemeral)
GalleryFooter(
currentPage: value,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
mediaSelectedCallBack: (val) {
_currentPage.value = val;
_pageController.animateToPage( _pageController.animateToPage(
val, val,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
Navigator.pop(context); Navigator.pop(context);
}); },
}, ),
), ],
], ),
), ),
), ),
], ],
@@ -264,9 +266,11 @@ class _FullScreenMediaState extends State<FullScreenMedia>
); );
@override @override
void dispose() async { void dispose() {
_animationController.dispose();
_pageController.dispose();
for (final package in videoPackages.values) { for (final package in videoPackages.values) {
await package.dispose(); package.dispose();
} }
super.dispose(); super.dispose();
} }
@@ -1816,6 +1816,8 @@ class MessageInputState extends State<MessageInput> {
_mentionedUsers.clear(); _mentionedUsers.clear();
message = _replaceUserNameWithId(message);
try { try {
Future sendingFuture; Future sendingFuture;
if (widget.editMessage == null || if (widget.editMessage == null ||
@@ -2081,3 +2083,21 @@ class _CountdownButton extends StatelessWidget {
), ),
); );
} }
Message _replaceUserNameWithId(Message message) {
final mentionedUsers = message.mentionedUsers;
if (mentionedUsers.isEmpty) return message;
var messageTextToSend = message.text;
if (messageTextToSend == null) return message;
for (final user in mentionedUsers.toSet()) {
final userName = user.name;
messageTextToSend = messageTextToSend!.replaceAll(
'@$userName',
'@${user.id}',
);
}
return message.copyWith(text: messageTextToSend);
}
@@ -712,20 +712,21 @@ class _MessageListViewState extends State<MessageListView> {
); );
}, },
), ),
BetterStreamBuilder<bool>( if (widget.showScrollToBottom)
stream: streamChannel!.channel.state!.isUpToDateStream, BetterStreamBuilder<bool>(
initialData: streamChannel!.channel.state!.isUpToDate, stream: streamChannel!.channel.state!.isUpToDateStream,
builder: (context, snapshot) => ValueListenableBuilder<bool>( initialData: streamChannel!.channel.state!.isUpToDate,
valueListenable: _showScrollToBottom, builder: (context, snapshot) => ValueListenableBuilder<bool>(
child: _buildScrollToBottom(), valueListenable: _showScrollToBottom,
builder: (context, value, child) { child: _buildScrollToBottom(),
if (!snapshot || value) { builder: (context, value, child) {
return child!; if (!snapshot || value) {
} return child!;
return const Offstage(); }
}, return const Offstage();
},
),
), ),
),
if (widget.showFloatingDateDivider) if (widget.showFloatingDateDivider)
_buildFloatingDateDivider(itemCount), _buildFloatingDateDivider(itemCount),
], ],
@@ -84,9 +84,10 @@ class MessageText extends StatelessWidget {
String _replaceMentions(String text) { String _replaceMentions(String text) {
var messageTextToRender = text; var messageTextToRender = text;
for (final user in message.mentionedUsers.toSet()) { for (final user in message.mentionedUsers.toSet()) {
final userId = user.id;
final userName = user.name; final userName = user.name;
messageTextToRender = messageTextToRender.replaceAll( messageTextToRender = messageTextToRender.replaceAll(
'@$userName', '@$userId',
'[@$userName](@${userName.replaceAll(' ', '')})', '[@$userName](@${userName.replaceAll(' ', '')})',
); );
} }
+3 -3
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/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. description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 3.4.0 version: 3.5.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -12,7 +12,7 @@ environment:
dependencies: dependencies:
cached_network_image: ^3.0.0 cached_network_image: ^3.0.0
characters: ^1.1.0 characters: ^1.1.0
chewie: ^1.2.0 chewie: ^1.3.0
collection: ^1.15.0 collection: ^1.15.0
diacritic: ^0.1.3 diacritic: ^0.1.3
dio: ^4.0.0 dio: ^4.0.0
@@ -36,7 +36,7 @@ dependencies:
rxdart: ^0.27.0 rxdart: ^0.27.0
share_plus: ^3.0.4 share_plus: ^3.0.4
shimmer: ^2.0.0 shimmer: ^2.0.0
stream_chat_flutter_core: ^3.4.0 stream_chat_flutter_core: ^3.5.0
substring_highlight: ^1.0.26 substring_highlight: ^1.0.26
synchronized: ^3.0.0 synchronized: ^3.0.0
url_launcher: ^6.0.3 url_launcher: ^6.0.3
@@ -1,3 +1,7 @@
## 3.5.0
- Updated `stream_chat` dependency to [`3.5.0`](https://pub.dev/packages/stream_chat/changelog).
## 3.4.0 ## 3.4.0
- Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 3.4.0 version: 3.5.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -16,7 +16,7 @@ dependencies:
sdk: flutter sdk: flutter
meta: ^1.3.0 meta: ^1.3.0
rxdart: ^0.27.0 rxdart: ^0.27.0
stream_chat: ^3.4.0 stream_chat: ^3.5.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.4.0 dart_code_metrics: ^4.4.0