Merge branch 'v4' into feat/stream-channel-listview

This commit is contained in:
Salvatore Giordano
2022-03-07 11:45:37 +01:00
41 changed files with 1508 additions and 315 deletions
+34 -9
View File
@@ -1,28 +1,47 @@
## Upcoming ## 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
🐞 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` are 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.
- [[#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
- Fix `channelState.pinnedMessagesStream` getting reset to `0` after a channel update.
- Fixed `unreadCount` after removing user from a channel.
🔄 Changed 🔄 Changed
- `client.location` is now deprecated in favor of the - `client.location` is now deprecated in favor of the
new [edge server](https://getstream.io/blog/chat-edge-infrastructure) and will be removed in v4.0.0. new [edge server](https://getstream.io/blog/chat-edge-infrastructure) and will be removed in v4.0.0.
- `channel.banUser`, `channel.unbanUser` is now deprecated in favor of the new `channel.banMember` - `channel.banUser`, `channel.unbanUser` is now deprecated in favor of the new `channel.banMember`
and `channel.unbanMember` and will be removed in v4.0.0. and `channel.unbanMember`. These deprecated methods will be removed in v4.0.0.
- Added `banExpires` property of type `DateTime` on the `Member`, `OwnUser`, and `User` models.
✅ Added ✅ Added
- Added `client.enrichUrl` endpoint for enriching URLs with metadata. - Added `client.enrichUrl` endpoint for enriching URLs with metadata.
- Fixed `unreadCount` after removing user from a channel.
- Added `client.queryBannedUsers`, `channel.queryBannedUsers` endpoint for querying banned users. - Added `client.queryBannedUsers`, `channel.queryBannedUsers` endpoint for querying banned users.
🐞 Fixed
- [[#871]](https://github.com/GetStream/stream-chat-flutter/issues/871) Fixed thread message deletion.
## 3.3.1 ## 3.3.1
🐞 Fixed 🐞 Fixed
@@ -45,6 +64,12 @@
- Fixed user presence indicator not updating correctly. - Fixed user presence indicator not updating correctly.
- `ChannelEvent.membersCount` defaults to 0 avoiding parsing errors due to missing `members_count` field. - `ChannelEvent.membersCount` defaults to 0 avoiding parsing errors due to missing `members_count` field.
## 3.2.1
🐞 Fixed
- Fixed `StreamChatClient.markAllRead` api call
## 3.2.0 ## 3.2.0
🐞 Fixed 🐞 Fixed
@@ -767,4 +792,4 @@
## 0.0.2 ## 0.0.2
- first beta version - first beta version
+146 -90
View File
@@ -419,7 +419,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);
} }
@@ -525,7 +525,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)) {
@@ -549,7 +549,7 @@ class Channel {
skipPush: skipPush, skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl, skipEnrichUrl: skipEnrichUrl,
); );
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) {
@@ -588,7 +588,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)) {
@@ -614,7 +614,7 @@ class Channel {
ownReactions: message.ownReactions, ownReactions: message.ownReactions,
); );
state?.addMessage(m); state?.updateMessage(m);
return response; return response;
} catch (e) { } catch (e) {
@@ -622,7 +622,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;
@@ -652,7 +652,7 @@ class Channel {
ownReactions: message.ownReactions, ownReactions: message.ownReactions,
); );
state?.addMessage(updatedMessage); state?.updateMessage(updatedMessage);
return response; return response;
} catch (e) { } catch (e) {
@@ -665,13 +665,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.
@@ -689,11 +694,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) {
@@ -833,6 +841,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 {
@@ -841,7 +850,7 @@ class Channel {
final now = DateTime.now(); final now = DateTime.now();
final user = _client.state.currentUser; final user = _client.state.currentUser;
final latestReactions = [...message.latestReactions ?? <Reaction>[]]; var latestReactions = [...message.latestReactions ?? <Reaction>[]];
if (enforceUnique) { if (enforceUnique) {
latestReactions.removeWhere((it) => it.userId == user!.id); latestReactions.removeWhere((it) => it.userId == user!.id);
} }
@@ -851,14 +860,21 @@ class Channel {
createdAt: now, createdAt: now,
type: type, type: type,
user: user, user: user,
score: 1, score: score,
extraData: extraData, extraData: extraData,
); );
// Inserting at the 0th index as it's the latest reaction latestReactions = (latestReactions
latestReactions.insert(0, newReaction); // Inserting at the 0th index as it's the latest reaction
final ownReactions = [...latestReactions] ..insert(0, newReaction))
..removeWhere((it) => it.userId != user!.id); .take(10)
.toList();
final ownReactions = enforceUnique
? <Reaction>[newReaction]
: <Reaction>[
...message.ownReactions ?? [],
newReaction,
];
final newMessage = message.copyWith( final newMessage = message.copyWith(
reactionCounts: {...message.reactionCounts ?? <String, int>{}} reactionCounts: {...message.reactionCounts ?? <String, int>{}}
@@ -875,19 +891,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;
} }
} }
@@ -898,7 +915,6 @@ class Channel {
Reaction reaction, Reaction reaction,
) async { ) async {
final type = reaction.type; final type = reaction.type;
final user = _client.state.currentUser;
final reactionCounts = {...message.reactionCounts ?? <String, int>{}}; final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
if (reactionCounts.containsKey(type)) { if (reactionCounts.containsKey(type)) {
@@ -915,8 +931,11 @@ class Channel {
r.type == reaction.type && r.type == reaction.type &&
r.messageId == reaction.messageId); r.messageId == reaction.messageId);
final ownReactions = [...latestReactions] final ownReactions = message.ownReactions
..removeWhere((it) => it.userId != user!.id); ?..removeWhere((r) =>
r.userId == reaction.userId &&
r.type == reaction.type &&
r.messageId == reaction.messageId);
final newMessage = message.copyWith( final newMessage = message.copyWith(
reactionCounts: reactionCounts..removeWhere((_, value) => value == 0), reactionCounts: reactionCounts..removeWhere((_, value) => value == 0),
@@ -925,7 +944,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(
@@ -935,7 +954,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;
} }
} }
@@ -1092,7 +1111,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
@@ -1312,7 +1331,7 @@ class Channel {
} }
/// Bans the user with given [userID] from the channel. /// Bans the user with given [userID] from the channel.
@Deprecated("Use 'banMember' instead") @Deprecated("Use 'banMember' instead. This method will be removed in v4.0.0")
Future<EmptyResponse> banUser( Future<EmptyResponse> banUser(
String userID, String userID,
Map<String, dynamic> options, Map<String, dynamic> options,
@@ -1334,7 +1353,9 @@ 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("Use 'unbanMember' instead") @Deprecated(
"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.
@@ -1545,16 +1566,21 @@ class ChannelClientState {
if (url == null || !url.contains('')) { if (url == null || !url.contains('')) {
return false; return false;
} }
final uri = Uri.parse(url); try {
if (!uri.host.endsWith('stream-io-cdn.com') || final uri = Uri.parse(url);
uri.queryParameters['Expires'] == null) { if (!uri.host.endsWith('stream-io-cdn.com') ||
uri.queryParameters['Expires'] == null) {
return false;
}
final secondsFromEpoch =
int.parse(uri.queryParameters['Expires']!);
final expiration = DateTime.fromMillisecondsSinceEpoch(
secondsFromEpoch * 1000,
);
return expiration.isBefore(DateTime.now());
} catch (_) {
return false; return false;
} }
final secondsFromEpoch =
int.parse(uri.queryParameters['Expires']!);
final expiration =
DateTime.fromMillisecondsSinceEpoch(secondsFromEpoch * 1000);
return expiration.isBefore(DateTime.now());
})) }))
.map((e) => e.id) .map((e) => e.id)
.toList(); .toList();
@@ -1702,23 +1728,36 @@ class ChannelClientState {
void _listenReactionDeleted() { void _listenReactionDeleted() {
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
final userId = _channel.client.state.currentUser!.id; final oldMessage =
messages.firstWhereOrNull((it) => it.id == event.message?.id) ??
threads[event.message?.parentId]
?.firstWhereOrNull((e) => e.id == event.message?.id);
final reaction = event.reaction;
final ownReactions = oldMessage?.ownReactions
?.whereNot((it) =>
it.type == reaction?.type &&
it.score == reaction?.score &&
it.messageId == reaction?.messageId &&
it.userId == reaction?.userId &&
it.extraData == reaction?.extraData)
.toList(growable: false);
final message = event.message!.copyWith( final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!] ownReactions: ownReactions,
..removeWhere((it) => it.userId != userId),
); );
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 userId = _channel.client.state.currentUser!.id; final oldMessage =
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: [...event.message!.latestReactions!] ownReactions: oldMessage?.ownReactions,
..removeWhere((it) => it.userId != userId),
); );
addMessage(message); updateMessage(message);
})); }));
} }
@@ -1729,12 +1768,14 @@ class ChannelClientState {
EventType.reactionUpdated, EventType.reactionUpdated,
) )
.listen((event) { .listen((event) {
final userId = _channel.client.state.currentUser!.id; final oldMessage =
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: [...event.message!.latestReactions!] ownReactions: oldMessage?.ownReactions,
..removeWhere((it) => it.userId != userId),
); );
addMessage(message); updateMessage(message);
if (message.pinned) { if (message.pinned) {
_channelState = _channelState.copyWith( _channelState = _channelState.copyWith(
@@ -1751,9 +1792,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);
} }
})); }));
} }
@@ -1768,7 +1809,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)) {
@@ -1778,9 +1819,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;
@@ -1795,8 +1840,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,
), ),
@@ -1809,41 +1870,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() {
@@ -1889,11 +1944,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 =>
@@ -2212,7 +2268,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,
)); ));
} }
@@ -2249,7 +2305,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(
@@ -159,7 +159,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 {
@@ -84,7 +84,10 @@ class ChannelApi {
/// Mark all channels for this user as read /// Mark all channels for this user as read
Future<EmptyResponse> markAllRead() async { Future<EmptyResponse> markAllRead() async {
final response = await _client.post('/channels/read'); final response = await _client.post(
'/channels/read',
data: {},
);
return EmptyResponse.fromJson(response.data); return EmptyResponse.fromJson(response.data);
} }
@@ -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);
@@ -7,6 +7,8 @@ import 'package:stream_chat/src/core/models/user.dart';
part 'channel_state.g.dart'; part 'channel_state.g.dart';
const _emptyPinnedMessages = <Message>[];
/// The class that contains the information about a channel /// The class that contains the information about a channel
@JsonSerializable() @JsonSerializable()
class ChannelState { class ChannelState {
@@ -15,7 +17,7 @@ class ChannelState {
this.channel, this.channel,
this.messages = const [], this.messages = const [],
this.members = const [], this.members = const [],
this.pinnedMessages = const [], this.pinnedMessages = _emptyPinnedMessages,
this.watcherCount, this.watcherCount,
this.watchers = const [], this.watchers = const [],
this.read = const [], this.read = const [],
@@ -54,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,
@@ -63,7 +65,11 @@ class ChannelState {
channel: channel ?? this.channel, channel: channel ?? this.channel,
messages: messages ?? this.messages, messages: messages ?? this.messages,
members: members ?? this.members, members: members ?? this.members,
pinnedMessages: pinnedMessages ?? this.pinnedMessages, // Hack to avoid using the default value in case nothing is provided.
// FIXME: Use non-nullable by default instead of empty list.
pinnedMessages: pinnedMessages == _emptyPinnedMessages
? this.pinnedMessages
: 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>))
@@ -69,7 +69,6 @@ class User extends Equatable {
'online', 'online',
'banned', 'banned',
'ban_expires', 'ban_expires',
'dashboard_ban_channel_cid',
'teams', 'teams',
'language', 'language',
]; ];
+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.3.1'; 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.3.1 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
@@ -1130,6 +1130,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 {
@@ -1257,6 +1386,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';
@@ -1363,6 +1675,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;
} }
} }
+16
View File
@@ -7,6 +7,22 @@
🐞 Fixed 🐞 Fixed
- Mentions overlay now doesn't overflow when not enough height available
## 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
- Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🐞 Fixed
- SVG rendering fixes. - SVG rendering fixes.
- Use file extension instead of mimeType for downloading files. - Use file extension instead of mimeType for downloading files.
- [[#860]](https://github.com/GetStream/stream-chat-flutter/issues/860) CastError while compressing Videos. - [[#860]](https://github.com/GetStream/stream-chat-flutter/issues/860) CastError while compressing Videos.
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
@@ -41,7 +41,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example" applicationId "com.example.example"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -16,6 +16,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
@@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.5.20' ext.kotlin_version = '1.6.0'
repositories { repositories {
google() google()
jcenter() jcenter()
@@ -596,7 +596,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),
), ),
), ),
@@ -609,7 +611,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;
} }
@@ -617,7 +619,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();
} }
@@ -356,6 +356,7 @@ class MessageInputState extends State<MessageInput>
with RestorationMixin<MessageInput> { with RestorationMixin<MessageInput> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
late FocusNode _focusNode = widget.focusNode ?? FocusNode(); late FocusNode _focusNode = widget.focusNode ?? FocusNode();
late final _isInternalFocusNode = widget.focusNode == null;
bool _inputEnabled = true; bool _inputEnabled = true;
bool get _commandEnabled => _effectiveController.value.command != null; bool get _commandEnabled => _effectiveController.value.command != null;
@@ -1168,26 +1169,31 @@ class MessageInputState extends State<MessageInput>
}; };
} }
return UserMentionsOverlay( return LayoutBuilder(
query: query, builder: (context, snapshot) => UserMentionsOverlay(
mentionAllAppUsers: widget.mentionAllAppUsers, query: query,
client: StreamChat.of(context).client, mentionAllAppUsers: widget.mentionAllAppUsers,
channel: channel, client: StreamChat.of(context).client,
size: Size(renderObject.size.width - 16, 400), channel: channel,
mentionsTileBuilder: tileBuilder, size: Size(
onMentionUserTap: (user) { renderObject.size.width - 16,
_effectiveController.addMentionedUser(user); min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()),
splits[splits.length - 1] = user.name; ),
final rejoin = splits.join('@'); mentionsTileBuilder: tileBuilder,
onMentionUserTap: (user) {
_effectiveController.addMentionedUser(user);
splits[splits.length - 1] = user.name;
final rejoin = splits.join('@');
_effectiveController.text = rejoin + _effectiveController.text = rejoin +
_effectiveController.text.substring( _effectiveController.text.substring(
_effectiveController.selectionStart, _effectiveController.selectionStart,
); );
_onChangedDebounced.cancel(); _onChangedDebounced.cancel();
setState(() => _showMentionsOverlay = false); setState(() => _showMentionsOverlay = false);
}, },
),
); );
} }
@@ -1816,6 +1822,7 @@ class MessageInputState extends State<MessageInput>
.removeListener(_onChangedDebounced); .removeListener(_onChangedDebounced);
_controller?.dispose(); _controller?.dispose();
_focusNode.removeListener(_focusNodeListener); _focusNode.removeListener(_focusNodeListener);
if (_isInternalFocusNode) _focusNode.dispose();
_stopSlowMode(); _stopSlowMode();
_onChangedDebounced.cancel(); _onChangedDebounced.cancel();
super.dispose(); super.dispose();
@@ -709,20 +709,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(' ', '')})',
); );
} }
+7 -7
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.3.2 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,22 +12,22 @@ 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
ezanimation: ^0.5.0 ezanimation: ^0.6.0
file_picker: ^4.1.3 file_picker: ^4.1.3
flutter: flutter:
sdk: flutter sdk: flutter
flutter_markdown: ^0.6.1 flutter_markdown: ^0.6.1
flutter_portal: ^0.4.0 flutter_portal: ^0.4.0
flutter_slidable: ^0.6.0 flutter_slidable: ^0.6.0
flutter_svg: ^0.23.0+1 flutter_svg: ^1.0.1
http_parser: ^4.0.0 http_parser: ^4.0.0
image_gallery_saver: ^1.7.0 image_gallery_saver: ^1.7.0
image_picker: ^0.8.2 image_picker: ^0.8.2
jiffy: ^4.1.0 jiffy: ^5.0.0
lottie: ^1.0.1 lottie: ^1.0.1
meta: ^1.3.0 meta: ^1.3.0
path_provider: ^2.0.1 path_provider: ^2.0.1
@@ -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.3.1 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
@@ -57,6 +57,6 @@ dev_dependencies:
dart_code_metrics: ^4.4.0 dart_code_metrics: ^4.4.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
golden_toolkit: ^0.11.0 golden_toolkit: ^0.13.0
mocktail: ^0.2.0 mocktail: ^0.2.0
path: ^1.8.0 path: ^1.8.0
@@ -1,4 +1,9 @@
## Upcoming ## 3.5.0
- Updated `stream_chat` dependency to [`3.5.0`](https://pub.dev/packages/stream_chat/changelog).
## 3.4.0
- Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog).
✅ Added ✅ Added
@@ -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.3.1 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
@@ -17,7 +17,7 @@ dependencies:
freezed_annotation: ^1.0.0 freezed_annotation: ^1.0.0
meta: ^1.3.0 meta: ^1.3.0
rxdart: ^0.27.0 rxdart: ^0.27.0
stream_chat: ^3.3.1 stream_chat: ^3.5.0
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
@@ -1,12 +1,16 @@
## 2.1.0 ## 2.1.0
✅ Added
* Added support for [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) locale.
🔄 Changed 🔄 Changed
* Some of the `Japanese` translations have been updated/changed for better understanding. * Some of the `Japanese` translations have been updated/changed for better understanding.
## 2.0.0 ## 2.0.0
- Updated `stream_chat_flutter` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog). * Updated `stream_chat_flutter` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
🐞 Fixed 🐞 Fixed
+8 -5
View File
@@ -37,6 +37,7 @@ At the moment we support the following languages:
- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) - [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
- [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) - [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart)
- [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) - [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart)
- [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart)
More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages.
@@ -74,6 +75,7 @@ class MyApp extends StatelessWidget {
Locale('es'), Locale('es'),
Locale('ja'), Locale('ja'),
Locale('ko'), Locale('ko'),
Locale('pt'),
], ],
// Add GlobalStreamChatLocalizations.delegates // Add GlobalStreamChatLocalizations.delegates
localizationsDelegates: GlobalStreamChatLocalizations.delegates, localizationsDelegates: GlobalStreamChatLocalizations.delegates,
@@ -112,13 +114,14 @@ Example:
```xml ```xml
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>nb</string> <string>hi</string>
<string>fr</string> <string>fr</string>
<string>it</string> <string>it</string>
<string>es</string> <string>es</string>
<string>ja</string> <string>ja</string>
<string>ko</string> <string>ko</string>
<string>pt</string>
</array> </array>
``` ```
@@ -4,10 +4,14 @@
<dict> <dict>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>it</string> <string>hi</string>
<string>fr</string> <string>fr</string>
<string>hi</string> <string>it</string>
<string>es</string>
<string>ja</string>
<string>ko</string>
<string>pt</string>
</array> </array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
@@ -75,6 +75,7 @@ class MyApp extends StatelessWidget {
Locale('es'), Locale('es'),
Locale('ja'), Locale('ja'),
Locale('ko'), Locale('ko'),
Locale('pt'),
], ],
// Add GlobalStreamChatLocalizations.delegates // Add GlobalStreamChatLocalizations.delegates
localizationsDelegates: GlobalStreamChatLocalizations.delegates, localizationsDelegates: GlobalStreamChatLocalizations.delegates,
@@ -100,6 +100,7 @@ class MyApp extends StatelessWidget {
Locale('es'), Locale('es'),
Locale('ja'), Locale('ja'),
Locale('ko'), Locale('ko'),
Locale('pt'),
], ],
// Add overridden "CustomStreamChatLocalizationsEn.delegate" along with // Add overridden "CustomStreamChatLocalizationsEn.delegate" along with
// "GlobalStreamChatLocalizations.delegates" // "GlobalStreamChatLocalizations.delegates"
@@ -17,6 +17,8 @@ part 'stream_chat_localizations_ko.dart';
part 'stream_chat_localizations_hi.dart'; part 'stream_chat_localizations_hi.dart';
part 'stream_chat_localizations_pt.dart';
/// The set of supported languages, as language code strings. /// The set of supported languages, as language code strings.
/// ///
/// The [GlobalStreamChatLocalizations.delegate] can generate localizations for /// The [GlobalStreamChatLocalizations.delegate] can generate localizations for
@@ -33,6 +35,7 @@ const kStreamChatSupportedLanguages = {
'es', 'es',
'ja', 'ja',
'ko', 'ko',
'pt',
}; };
/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`.
@@ -69,6 +72,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsJa(); return const StreamChatLocalizationsJa();
case 'ko': case 'ko':
return const StreamChatLocalizationsKo(); return const StreamChatLocalizationsKo();
case 'pt':
return const StreamChatLocalizationsPt();
} }
} }
@@ -0,0 +1,374 @@
part of 'stream_chat_localizations.dart';
/// The translations for Portuguese (`pt`).
class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations {
/// Create an instance of the translation bundle for Portuguese.
const StreamChatLocalizationsPt({String localeName = 'pt'})
: super(localeName: localeName);
@override
String get launchUrlError => 'O URL não pôde ser aberto';
@override
String get loadingUsersError => 'Erro de carregamento do usuário';
@override
String get noUsersLabel => 'Nenhum usuário atualmente';
@override
String get retryLabel => 'Tente novamente';
@override
String get userLastOnlineText => 'Última vez on-line';
@override
String get userOnlineText => 'Online';
@override
String userTypingText(Iterable<User> users) {
if (users.isEmpty) return '';
final first = users.first;
if (users.length == 1) {
return '${first.name} está digitando';
}
return '${first.name} e ${users.length - 1} estão digitando';
}
@override
String get threadReplyLabel => 'Responder na conversa';
@override
String get onlyVisibleToYouText => 'Visível apenas para você';
@override
String threadReplyCountText(int count) => '$count respostas na conversa';
@override
String attachmentsUploadProgressText({
required int remaining,
required int total,
}) =>
'Tranferência em andamento $remaining/$total ...';
@override
String pinnedByUserText({
required User pinnedBy,
required User currentUser,
}) {
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
if (pinnedByCurrentUser) return 'Definido por você';
return 'Definido por ${pinnedBy.name}';
}
@override
String get emptyMessagesText => 'Não há mensagens';
@override
String get genericErrorText => 'Ocorreu um problema';
@override
String get loadingMessagesError => 'Ocorreu um problema ao carregar mensagem';
@override
String resultCountText(int count) => '$count resultados';
@override
String get messageDeletedText => 'Esta mensagem foi excluída.';
@override
String get messageDeletedLabel => 'Mensagem excluída';
@override
String get messageReactionsLabel => 'Reações às mensagens';
@override
String get emptyChatMessagesText => 'Ainda não há mensagens aqui...';
@override
String threadSeparatorText(int replyCount) {
if (replyCount == 1) return '1 resposta';
return '$replyCount respostas';
}
@override
String get connectedLabel => 'Conectado';
@override
String get disconnectedLabel => 'Desconectado';
@override
String get reconnectingLabel => 'Reconectando...';
@override
String get alsoSendAsDirectMessageLabel =>
'Enviar também como mensagem direta';
@override
String get addACommentOrSendLabel => 'Adicionar um comnetário ou enviar';
@override
String get searchGifLabel => 'Pesquisar GIFs';
@override
String get writeAMessageLabel => 'Escrever uma mensagem';
@override
String get instantCommandsLabel => 'Comandos instantâneos';
@override
String fileTooLargeAfterCompressionError(double limitInMB) =>
'O arquivo é muito grande para carregamento. '
'O tamanho máximo do arquivo é de $limitInMB MB. '
'Tentamos comprimi-lo, mas não foi suficiente.';
@override
String fileTooLargeError(double limitInMB) =>
'O arquivo é muito grande para carregamento. '
'O tamanho máximo dos arquivos é de $limitInMB MB.';
@override
String emojiMatchingQueryText(String query) =>
'Emoji correspondente a "$query"';
@override
String get addAFileLabel => 'Adicionar um arquivo';
@override
String get photoFromCameraLabel => 'Foto da câmera';
@override
String get uploadAFileLabel => 'Transferir um arquivo';
@override
String get uploadAPhotoLabel => 'Carregar uma foto';
@override
String get uploadAVideoLabel => 'Carregar um vídeo';
@override
String get videoFromCameraLabel => 'Vídeo da câmera';
@override
String get okLabel => 'OK';
@override
String get somethingWentWrongError => 'Algo deu errado';
@override
String get addMoreFilesLabel => 'Adicionar mais arquivos';
@override
String get enablePhotoAndVideoAccessMessage =>
'Por favor, permita o acesso a suas fotos'
'\ne vídeos para que possa compartilhar com sua rede.';
@override
String get allowGalleryAccessMessage => 'Permitir acesso à sua galeria';
@override
String get flagMessageLabel => 'Denunciar mensagem';
@override
String get flagMessageQuestion => 'Gostaria de enviar esta mensagem ao'
'\nmoderador para maior investigação?';
@override
String get flagLabel => 'DENUNCIAR';
@override
String get cancelLabel => 'CANCELAR';
@override
String get flagMessageSuccessfulLabel => 'Mensagem denunciada';
@override
String get flagMessageSuccessfulText =>
'Esta mensagem foi enviada a um moderador.';
@override
String get deleteLabel => 'APAGAR';
@override
String get deleteMessageLabel => 'Apagar mensagem';
@override
String get deleteMessageQuestion =>
'Você tem certeza que deseja apagar essa\nmensagem permanentemente?';
@override
String get operationCouldNotBeCompletedText =>
'A operação não pode ser completada.';
@override
String get replyLabel => 'Resposta';
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Desafixar na conversa';
return 'Fixar na conversa';
}
@override
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
if (isDeleteFailed) return 'Repetir apagar mensagem';
return 'Apagar mensagem';
}
@override
String get copyMessageLabel => 'Copiar mensagem';
@override
String get editMessageLabel => 'Editar mensagem';
@override
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
if (isUpdateFailed) return 'Reenviar mensagem alterada';
return 'Reenviar';
}
@override
String get photosLabel => 'Fotos';
String _getDay(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = DateTime(now.year, now.month, now.day - 1);
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (date == today) {
return 'Hoje';
} else if (date == yesterday) {
return 'Ontem';
} else {
return 'o ${Jiffy(date).MMMd}';
}
}
@override
String sentAtText({required DateTime date, required DateTime time}) =>
'''Enviado ${_getDay(date)} às ${Jiffy(time.toLocal()).format('HH:mm')}''';
@override
String get todayLabel => 'Hoje';
@override
String get yesterdayLabel => 'Ontem';
@override
String get channelIsMutedText => 'O canal está silenciado';
@override
String get noTitleText => 'Sem título';
@override
String get letsStartChattingLabel => 'Vamos começar a conversar!';
@override
String get sendingFirstMessageLabel =>
'Que tal enviar sua primeira mensagem a um amigo?';
@override
String get startAChatLabel => 'Iniciar uma conversa';
@override
String get loadingChannelsError => 'Erro ao carregar os canais';
@override
String get deleteConversationLabel => 'Apagar a conversa';
@override
String get deleteConversationQuestion =>
'Tem certeza que deseja apagar essa conversa?';
@override
String get streamChatLabel => 'Stream Chat';
@override
String get searchingForNetworkText => 'Pesquisando rede';
@override
String get offlineLabel => 'Sem conexão...';
@override
String get tryAgainLabel => 'Tente novamente';
@override
String membersCountText(int count) {
if (count == 1) return '1 membro';
return '$count membros';
}
@override
String watchersCountText(int count) {
if (count == 1) return '1 online';
return '$count online';
}
@override
String get viewInfoLabel => 'Ver informação';
@override
String get leaveGroupLabel => 'Sair do grupo';
@override
String get leaveLabel => 'SAIR';
@override
String get leaveConversationLabel => 'Sair da conversa';
@override
String get leaveConversationQuestion =>
'Tem certeza que deseja sair dessa conversa?';
@override
String get showInChatLabel => 'Mostrar no chat';
@override
String get saveImageLabel => 'Salvar imagem';
@override
String get saveVideoLabel => 'Salvar vídeo';
@override
String get uploadErrorLabel => 'ERRO DE TRANSFERÊNCIA';
@override
String get giphyLabel => 'Giphy';
@override
String get shuffleLabel => 'Misturar';
@override
String get sendLabel => 'Enviar';
@override
String get withText => 'com';
@override
String get inText => 'em';
@override
String get youText => 'Você';
@override
String galleryPaginationText({
required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} de $totalPages';
@override
String get fileText => 'Arquivo';
@override
String get replyToMessageLabel => 'Responder à mensagem';
@override
String attachmentLimitExceedError(int limit) => '''
Não é possível adicionar mais de $limit arquivos de uma vez
''';
@override
String get slowModeOnLabel => 'Modo lento ativado';
}
@@ -1,6 +1,6 @@
name: stream_chat_localizations name: stream_chat_localizations
description: The Official localizations for Stream Chat Flutter, a service for building chat applications description: The Official localizations for Stream Chat Flutter, a service for building chat applications
version: 2.0.0 version: 2.1.0
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
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
@@ -14,7 +14,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
stream_chat_flutter: ^3.0.0 stream_chat_flutter: ^3.4.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.4.0 dart_code_metrics: ^4.4.0
@@ -1,4 +1,4 @@
## Upcoming ## 3.1.0
- Bump `drift` to `1.3.0`. - Bump `drift` to `1.3.0`.
@@ -1,7 +1,7 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
version: 3.0.0 version: 3.1.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
@@ -10,7 +10,7 @@ environment:
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
drift: 1.3.0 drift: ">=1.3.0 < 1.4.0"
flutter: flutter:
sdk: flutter sdk: flutter
logging: ^1.0.1 logging: ^1.0.1
@@ -19,12 +19,12 @@ dependencies:
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.1 path_provider: ^2.0.1
sqlite3_flutter_libs: ^0.5.0 sqlite3_flutter_libs: ^0.5.0
stream_chat: ^3.0.0 stream_chat: ^3.4.0
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
dart_code_metrics: ^4.4.0 dart_code_metrics: ^4.4.0
drift_dev: 1.3.0 drift_dev: ">=1.3.0 < 1.4.0"
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.2.0 mocktail: ^0.2.0