Merge pull request #884 from GetStream/release/3.4.0

chore(llc,core,ui,persistence,localizations): 3.4.0
This commit is contained in:
Salvatore Giordano
2022-02-03 14:59:13 +01:00
committed by GitHub
135 changed files with 1474 additions and 572 deletions
+27 -1
View File
@@ -1,3 +1,29 @@
## 3.4.0
🐞 Fixed
- [[#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.
- [[#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
- `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.
- `channel.banUser`, `channel.unbanUser` is now deprecated in favor of the new `channel.banMember`
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 `client.enrichUrl` endpoint for enriching URLs with metadata.
- Added `client.queryBannedUsers`, `channel.queryBannedUsers` endpoint for querying banned users.
## 3.3.1
🐞 Fixed
@@ -748,4 +774,4 @@
## 0.0.2
- first beta version
- first beta version
+138 -33
View File
@@ -4,15 +4,9 @@ import 'dart:math';
import 'package:collection/collection.dart'
show IterableExtension, ListEquality;
import 'package:dio/dio.dart';
import 'package:rate_limiter/rate_limiter.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/retry_queue.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/util/utils.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/stream_chat.dart';
/// Class that manages a specific channel.
@@ -825,7 +819,7 @@ class Channel {
final now = DateTime.now();
final user = _client.state.currentUser;
final latestReactions = [...message.latestReactions ?? <Reaction>[]];
var latestReactions = [...message.latestReactions ?? <Reaction>[]];
if (enforceUnique) {
latestReactions.removeWhere((it) => it.userId == user!.id);
}
@@ -839,10 +833,17 @@ class Channel {
extraData: extraData,
);
// Inserting at the 0th index as it's the latest reaction
latestReactions.insert(0, newReaction);
final ownReactions = [...latestReactions]
..removeWhere((it) => it.userId != user!.id);
latestReactions = (latestReactions
// Inserting at the 0th index as it's the latest reaction
..insert(0, newReaction))
.take(10)
.toList();
final ownReactions = enforceUnique
? <Reaction>[newReaction]
: <Reaction>[
...message.ownReactions ?? [],
newReaction,
];
final newMessage = message.copyWith(
reactionCounts: {...message.reactionCounts ?? <String, int>{}}
@@ -882,7 +883,6 @@ class Channel {
Reaction reaction,
) async {
final type = reaction.type;
final user = _client.state.currentUser;
final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
if (reactionCounts.containsKey(type)) {
@@ -899,8 +899,11 @@ class Channel {
r.type == reaction.type &&
r.messageId == reaction.messageId);
final ownReactions = [...latestReactions]
..removeWhere((it) => it.userId != user!.id);
final ownReactions = message.ownReactions
?..removeWhere((r) =>
r.userId == reaction.userId &&
r.type == reaction.type &&
r.messageId == reaction.messageId);
final newMessage = message.copyWith(
reactionCounts: reactionCounts..removeWhere((_, value) => value == 0),
@@ -1268,6 +1271,21 @@ class Channel {
pagination: pagination,
);
/// Query channel banned users.
Future<QueryBannedUsersResponse> queryBannedUsers({
Filter? filter,
List<SortOption>? sort,
PaginationParams? pagination,
}) {
_checkInitialized();
filter ??= Filter.equal('channel_cid', cid!);
return _client.queryBannedUsers(
filter: filter,
sort: sort,
pagination: pagination,
);
}
/// Mutes the channel.
Future<EmptyResponse> mute({Duration? expiration}) {
_checkInitialized();
@@ -1281,9 +1299,17 @@ class Channel {
}
/// Bans the user with given [userID] from the channel.
@Deprecated("Use 'banMember' instead. This method will be removed in v4.0.0")
Future<EmptyResponse> banUser(
String userID,
Map<String, dynamic> options,
) =>
banMember(userID, options);
/// Bans the member with given [userID] from the channel.
Future<EmptyResponse> banMember(
String userID,
Map<String, dynamic> options,
) async {
_checkInitialized();
final opts = Map<String, dynamic>.from(options)
@@ -1295,7 +1321,12 @@ class Channel {
}
/// Remove the ban for the user with given [userID] in the channel.
Future<EmptyResponse> unbanUser(String userID) async {
@Deprecated(
"Use 'unbanMember' instead. This method will be removed in v4.0.0")
Future<EmptyResponse> unbanUser(String userID) => unbanMember(userID);
/// Remove the ban for the member with given [userID] in the channel.
Future<EmptyResponse> unbanMember(String userID) async {
_checkInitialized();
return _client.unbanUser(userID, {
'type': type,
@@ -1466,6 +1497,10 @@ class ChannelClientState {
_listenMemberRemoved();
_listenMemberBanned();
_listenMemberUnbanned();
_startCleaning();
_startCleaningPinnedMessages();
@@ -1498,16 +1533,21 @@ class ChannelClientState {
if (url == null || !url.contains('')) {
return false;
}
final uri = Uri.parse(url);
if (!uri.host.endsWith('stream-io-cdn.com') ||
uri.queryParameters['Expires'] == null) {
try {
final uri = Uri.parse(url);
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;
}
final secondsFromEpoch =
int.parse(uri.queryParameters['Expires']!);
final expiration =
DateTime.fromMillisecondsSinceEpoch(secondsFromEpoch * 1000);
return expiration.isBefore(DateTime.now());
}))
.map((e) => e.id)
.toList();
@@ -1538,6 +1578,7 @@ class ChannelClientState {
members: List.from(
channelState.members..removeWhere((m) => m.userId == user!.id),
),
read: channelState.read..removeWhere((r) => r.user.id == user!.id),
));
}));
}
@@ -1563,6 +1604,54 @@ class ChannelClientState {
}));
}
void _listenMemberBanned() {
_subscriptions.add(_channel
.on(EventType.userBanned)
.where((it) => it.cid != null) // filters channel ban from app ban
.listen(
(event) async {
final user = event.user!;
final member = await _channel
.queryMembers(filter: Filter.equal('id', user.id))
.then((it) => it.members.first);
_updateMember(member);
},
));
}
void _listenMemberUnbanned() {
_subscriptions.add(_channel
.on(EventType.userUnbanned)
.where((it) => it.cid != null) // filters channel ban from app ban
.listen(
(event) async {
final user = event.user!;
final member = await _channel
.queryMembers(filter: Filter.equal('id', user.id))
.then((it) => it.members.first);
_updateMember(member);
},
));
}
void _updateMember(Member member) {
final currentMembers = [...members];
final memberIndex = currentMembers.indexWhere(
(m) => m.userId == member.userId,
);
if (memberIndex == -1) return;
currentMembers[memberIndex] = member;
updateChannelState(
channelState.copyWith(
members: currentMembers,
),
);
}
/// Flag which indicates if [ChannelClientState] contain latest/recent messages or not.
///
/// This flag should be managed by UI sdks.
@@ -1604,10 +1693,19 @@ class ChannelClientState {
void _listenReactionDeleted() {
_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);
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(
ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId),
ownReactions: ownReactions,
);
addMessage(message);
}));
@@ -1615,10 +1713,10 @@ class ChannelClientState {
void _listenReactions() {
_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);
final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId),
ownReactions: oldMessage?.ownReactions,
);
addMessage(message);
}));
@@ -1631,10 +1729,11 @@ class ChannelClientState {
EventType.reactionUpdated,
)
.listen((event) {
final userId = _channel.client.state.currentUser!.id;
final oldMessage =
messages.firstWhereOrNull((it) => it.id == event.message?.id);
final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId),
ownReactions: oldMessage?.ownReactions,
);
addMessage(message);
@@ -1729,7 +1828,13 @@ class ChannelClientState {
if (replyCount == null || replyCount == 0) return;
addMessage(parentMessage.copyWith(replyCount: replyCount - 1));
updateThreadInfo(parentId, threads[parentId]!..remove(message));
updateThreadInfo(
parentId,
threads[parentId]!
..removeWhere(
(e) => e.id == message.id,
),
);
} else {
// Remove regular message
final allMessages = [...messages];
@@ -66,8 +66,11 @@ class StreamChatClient {
this.logLevel = Level.WARNING,
LogHandlerFunction? logHandlerFunction,
RetryPolicy? retryPolicy,
Location? location,
@Deprecated('Use location to change baseUrl instead') String? baseURL,
@Deprecated('''
Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0.
Read more here: https://getstream.io/blog/chat-edge-infrastructure
''') Location? location,
String? baseURL,
Duration connectTimeout = const Duration(seconds: 6),
Duration receiveTimeout = const Duration(seconds: 6),
StreamChatApi? chatApi,
@@ -79,7 +82,6 @@ class StreamChatClient {
final options = StreamHttpClientOptions(
baseUrl: baseURL,
location: location,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: {'X-Stream-Client': defaultUserAgent},
@@ -685,6 +687,18 @@ class StreamChatClient {
return response;
}
/// Query banned users.
Future<QueryBannedUsersResponse> queryBannedUsers({
required Filter filter,
List<SortOption>? sort,
PaginationParams? pagination,
}) =>
_chatApi.moderation.queryBannedUsers(
filter: filter,
sort: sort,
pagination: pagination,
);
/// A message search.
Future<SearchMessagesResponse> search(
Filter filter, {
@@ -1316,6 +1330,10 @@ class StreamChatClient {
},
);
/// Get OpenGraph data of the given [url].
Future<OGAttachmentResponse> enrichUrl(String url) =>
_chatApi.general.enrichUrl(url);
/// Closes the [_ws] connection and resets the [state]
/// If [flushChatPersistence] is true the client deletes all offline
/// user's data.
@@ -1,13 +1,8 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
@@ -96,4 +96,16 @@ class GeneralApi {
return QueryMembersResponse.fromJson(response.data);
}
/// Get OpenGraph data of the given [url].
Future<OGAttachmentResponse> enrichUrl(String url) async {
final response = await _client.get(
'/og',
queryParameters: {
'url': url,
},
);
return OGAttachmentResponse.fromJson(response.data);
}
}
@@ -1,5 +1,7 @@
import 'package:stream_chat/src/core/api/responses.dart';
import 'dart:convert';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/stream_chat.dart';
/// Defines the api dedicated to moderation operations
class ModerationApi {
@@ -125,4 +127,24 @@ class ModerationApi {
);
return EmptyResponse.fromJson(response.data);
}
/// Queries banned users.
Future<QueryBannedUsersResponse> queryBannedUsers({
Filter? filter,
List<SortOption>? sort,
PaginationParams? pagination,
}) async {
final response = await _client.get(
'/query_banned_users',
queryParameters: {
'payload': jsonEncode({
if (sort != null) 'sort': sort,
if (filter != null) 'filter_conditions': filter,
if (pagination != null) ...pagination.toJson(),
}),
},
);
return QueryBannedUsersResponse.fromJson(response.data);
}
}
@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/client/client.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/banned_user.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/device.dart';
@@ -106,6 +107,18 @@ class QueryUsersResponse extends _BaseResponse {
_$QueryUsersResponseFromJson(json);
}
/// Model response for [StreamChatClient.queryBannedUsers] api call
@JsonSerializable(createToJson: false)
class QueryBannedUsersResponse extends _BaseResponse {
/// List of users returned by the query
@JsonKey(defaultValue: [])
late List<BannedUser> bans;
/// Create a new instance from a json
static QueryBannedUsersResponse fromJson(Map<String, dynamic> json) =>
_$QueryBannedUsersResponseFromJson(json);
}
/// Model response for [channel.getReactions] api call
@JsonSerializable(createToJson: false)
class QueryReactionsResponse extends _BaseResponse {
@@ -442,3 +455,43 @@ class ChannelStateResponse extends _BaseResponse {
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
_$ChannelStateResponseFromJson(json);
}
/// Model response for [Client.enrichUrl] api call.
@JsonSerializable(createToJson: false)
class OGAttachmentResponse extends _BaseResponse {
/// The URL of the page that was scraped.
late String ogScrapeUrl;
/// The URL of the asset.
String? assetUrl;
/// The URL of the author.
String? authorLink;
/// The name of the author.
String? authorName;
/// The URL of the image.
String? imageUrl;
/// The text of the attachment.
String? text;
/// The URL of the thumbnail.
String? thumbUrl;
/// The title of the attachment.
String? title;
/// The URL of the title.
String? titleLink;
/// The type of the attachment.
///
/// 'video' | 'audio' | 'image'
String? type;
/// Create a new instance from a [json].
static OGAttachmentResponse fromJson(Map<String, dynamic> json) =>
_$OGAttachmentResponseFromJson(json);
}
@@ -62,6 +62,15 @@ QueryUsersResponse _$QueryUsersResponseFromJson(Map<String, dynamic> json) =>
.toList() ??
[];
QueryBannedUsersResponse _$QueryBannedUsersResponseFromJson(
Map<String, dynamic> json) =>
QueryBannedUsersResponse()
..duration = json['duration'] as String?
..bans = (json['bans'] as List<dynamic>?)
?.map((e) => BannedUser.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
QueryReactionsResponse _$QueryReactionsResponseFromJson(
Map<String, dynamic> json) =>
QueryReactionsResponse()
@@ -273,3 +282,18 @@ ChannelStateResponse _$ChannelStateResponseFromJson(
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
OGAttachmentResponse _$OGAttachmentResponseFromJson(
Map<String, dynamic> json) =>
OGAttachmentResponse()
..duration = json['duration'] as String?
..ogScrapeUrl = json['og_scrape_url'] as String
..assetUrl = json['asset_url'] as String?
..authorLink = json['author_link'] as String?
..authorName = json['author_name'] as String?
..imageUrl = json['image_url'] as String?
..text = json['text'] as String?
..thumbUrl = json['thumb_url'] as String?
..title = json['title'] as String?
..titleLink = json['title_link'] as String?
..type = json['type'] as String?;
@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:stream_chat/src/core/error/chat_error_code.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
@@ -11,7 +11,6 @@ import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/location.dart';
part 'stream_http_client_options.dart';
@@ -1,32 +1,20 @@
part of 'stream_http_client.dart';
const _defaultBaseURL = 'https://chat-us-east-1.stream-io-api.com';
const _defaultBaseURL = 'https://chat.stream-io-api.com';
/// Client options to modify [StreamHttpClient]
class StreamHttpClientOptions {
/// Instantiates a new [StreamHttpClientOptions]
const StreamHttpClientOptions({
String? baseUrl,
this.location,
this.connectTimeout = const Duration(seconds: 6),
this.receiveTimeout = const Duration(seconds: 6),
this.queryParameters = const {},
this.headers = const {},
}) : _baseUrl = baseUrl ?? _defaultBaseURL;
final String _baseUrl;
}) : baseUrl = baseUrl ?? _defaultBaseURL;
/// base url to use with client.
String get baseUrl {
if (location == null) return _baseUrl;
const serviceName = 'chat';
final locationName = location!.name;
const baseDomainName = 'stream-io-api.com';
return 'https://$serviceName-proxy-$locationName.$baseDomainName';
}
/// data center to use with client
final Location? location;
final String baseUrl;
/// connect timeout, default to 6s
final Duration connectTimeout;
@@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:dio/dio.dart' show MultipartFile;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http_parser/http_parser.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
import 'package:stream_chat/src/core/util/extension.dart';
@@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/user.dart';
part 'banned_user.g.dart';
/// Contains information about a [User] that was banned from a [Channel] or App.
@JsonSerializable()
class BannedUser extends Equatable {
/// Creates a new instance of [BannedUser]
const BannedUser({
required this.user,
this.bannedBy,
this.channel,
this.createdAt,
this.expires,
this.shadow = false,
this.reason,
});
/// Create a new instance from a json
factory BannedUser.fromJson(Map<String, dynamic> json) =>
_$BannedUserFromJson(json);
/// Banned user.
final User user;
/// User that banned the [user].
final User? bannedBy;
/// Channel where the [user] was banned.
final ChannelModel? channel;
/// Timestamp when the [user] was banned.
final DateTime? createdAt;
/// Timestamp when the [user] will be unbanned.
final DateTime? expires;
/// Whether the [user] is a shadow banned user.
final bool shadow;
/// Reason for the ban.
final String? reason;
/// Serialize to json
Map<String, dynamic> toJson() => _$BannedUserToJson(this);
/// Returns a copy of this object with the given fields updated.
BannedUser copyWith({
User? user,
User? bannedBy,
ChannelModel? channel,
DateTime? createdAt,
DateTime? expires,
bool? shadow,
String? reason,
}) =>
BannedUser(
user: user ?? this.user,
bannedBy: bannedBy ?? this.bannedBy,
channel: channel ?? this.channel,
createdAt: createdAt ?? this.createdAt,
expires: expires ?? this.expires,
shadow: shadow ?? this.shadow,
reason: reason ?? this.reason,
);
@override
List<Object?> get props => [
user,
bannedBy,
channel,
createdAt,
expires,
shadow,
reason,
];
}
@@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'banned_user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BannedUser _$BannedUserFromJson(Map<String, dynamic> json) => BannedUser(
user: User.fromJson(json['user'] as Map<String, dynamic>),
bannedBy: json['banned_by'] == null
? null
: User.fromJson(json['banned_by'] as Map<String, dynamic>),
channel: json['channel'] == null
? null
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
createdAt: json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
expires: json['expires'] == null
? null
: DateTime.parse(json['expires'] as String),
shadow: json['shadow'] as bool? ?? false,
reason: json['reason'] as String?,
);
Map<String, dynamic> _$BannedUserToJson(BannedUser instance) =>
<String, dynamic>{
'user': instance.user.toJson(),
'banned_by': instance.bannedBy?.toJson(),
'channel': instance.channel?.toJson(),
'created_at': instance.createdAt?.toIso8601String(),
'expires': instance.expires?.toIso8601String(),
'shadow': instance.shadow,
'reason': instance.reason,
};
@@ -7,6 +7,8 @@ import 'package:stream_chat/src/core/models/user.dart';
part 'channel_state.g.dart';
const _emptyPinnedMessages = <Message>[];
/// The class that contains the information about a channel
@JsonSerializable()
class ChannelState {
@@ -15,7 +17,7 @@ class ChannelState {
this.channel,
this.messages = const [],
this.members = const [],
this.pinnedMessages = const [],
this.pinnedMessages = _emptyPinnedMessages,
this.watcherCount,
this.watchers = const [],
this.read = const [],
@@ -63,7 +65,11 @@ class ChannelState {
channel: channel ?? this.channel,
messages: messages ?? this.messages,
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 ?? _emptyPinnedMessages,
watcherCount: watcherCount ?? this.watcherCount,
watchers: watchers ?? this.watchers,
read: read ?? this.read,
@@ -1,6 +1,4 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -20,6 +20,7 @@ class Member extends Equatable {
DateTime? createdAt,
DateTime? updatedAt,
this.banned = false,
this.banExpires,
this.shadowBanned = false,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
@@ -56,6 +57,9 @@ class Member extends Equatable {
/// True if the member is banned from the channel
final bool banned;
/// The date at which the ban will expire.
final DateTime? banExpires;
/// True if the member is shadow banned from the channel
final bool shadowBanned;
@@ -77,6 +81,7 @@ class Member extends Equatable {
DateTime? createdAt,
DateTime? updatedAt,
bool? banned,
DateTime? banExpires,
bool? shadowBanned,
}) =>
Member(
@@ -85,6 +90,7 @@ class Member extends Equatable {
inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt,
invited: invited ?? this.invited,
banned: banned ?? this.banned,
banExpires: banExpires ?? this.banExpires,
shadowBanned: shadowBanned ?? this.shadowBanned,
role: role ?? this.role,
userId: userId ?? this.userId,
@@ -106,6 +112,7 @@ class Member extends Equatable {
userId,
isModerator,
banned,
banExpires,
shadowBanned,
createdAt,
updatedAt,
@@ -27,6 +27,9 @@ Member _$MemberFromJson(Map<String, dynamic> json) => Member(
? null
: DateTime.parse(json['updated_at'] as String),
banned: json['banned'] as bool? ?? false,
banExpires: json['ban_expires'] == null
? null
: DateTime.parse(json['ban_expires'] as String),
shadowBanned: json['shadow_banned'] as bool? ?? false,
);
@@ -39,6 +42,7 @@ Map<String, dynamic> _$MemberToJson(Member instance) => <String, dynamic>{
'user_id': instance.userId,
'is_moderator': instance.isModerator,
'banned': instance.banned,
'ban_expires': instance.banExpires?.toIso8601String(),
'shadow_banned': instance.shadowBanned,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
@@ -1,7 +1,4 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/device.dart';
import 'package:stream_chat/src/core/models/mute.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -29,6 +26,7 @@ class OwnUser extends User {
bool online = false,
Map<String, Object?> extraData = const {},
bool banned = false,
DateTime? banExpires,
List<String> teams = const [],
String? language,
}) : super(
@@ -42,6 +40,7 @@ class OwnUser extends User {
online: online,
extraData: extraData,
banned: banned,
banExpires: banExpires,
teams: teams,
language: language,
);
@@ -78,6 +77,7 @@ class OwnUser extends User {
bool? online,
Map<String, Object?>? extraData,
bool? banned,
DateTime? banExpires,
List<String>? teams,
List<Mute>? channelMutes,
List<Device>? devices,
@@ -89,11 +89,12 @@ class OwnUser extends User {
OwnUser(
id: id ?? this.id,
role: role ?? this.role,
/* if null, it will be retrieved from extraData['name']*/
// if null, it will be retrieved from extraData['name']
name: name,
/* if null, it will be retrieved from extraData['image']*/
// if null, it will be retrieved from extraData['image']
image: image,
banned: banned ?? this.banned,
banExpires: banExpires ?? this.banExpires,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastActive: lastActive ?? this.lastActive,
@@ -139,7 +140,7 @@ class OwnUser extends User {
@JsonKey(includeIfNull: false)
final List<Mute> mutes;
/// List of users muted by the user.
/// List of channels muted by the user.
@JsonKey(includeIfNull: false)
final List<Mute> channelMutes;
@@ -35,6 +35,9 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) => OwnUser(
online: json['online'] as bool? ?? false,
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
banned: json['banned'] as bool? ?? false,
banExpires: json['ban_expires'] == null
? null
: DateTime.parse(json['ban_expires'] as String),
teams:
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const [],
@@ -41,11 +41,12 @@ class User extends Equatable {
Map<String, Object?> extraData = const {},
this.online = false,
this.banned = false,
this.banExpires,
this.teams = const [],
this.language,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(),
/*For backwards compatibility, set 'name', 'image' in [extraData].*/
// For backwards compatibility, set 'name', 'image' in [extraData].
extraData = {
...extraData,
if (name != null) 'name': name,
@@ -67,6 +68,7 @@ class User extends Equatable {
'last_active',
'online',
'banned',
'ban_expires',
'teams',
'language',
];
@@ -129,14 +131,18 @@ class User extends Equatable {
)
final bool banned;
/// Map of custom user extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// The date at which the ban will expire.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime? banExpires;
/// The language this user prefers.
@JsonKey(includeIfNull: false)
final String? language;
/// Map of custom user extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// List of users to list of userIds.
static List<String>? toIds(List<User>? users) =>
users?.map((u) => u.id).toList();
@@ -158,15 +164,16 @@ class User extends Equatable {
bool? online,
Map<String, Object?>? extraData,
bool? banned,
DateTime? banExpires,
List<String>? teams,
String? language,
}) =>
User(
id: id ?? this.id,
role: role ?? this.role,
/* if null, it will be retrieved from extraData['name']*/
// if null, it will be retrieved from extraData['name']
name: name,
/* if null, it will be retrieved from extraData['image']*/
// if null, it will be retrieved from extraData['image']
image: image,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
@@ -174,6 +181,7 @@ class User extends Equatable {
online: online ?? this.online,
extraData: extraData ?? this.extraData,
banned: banned ?? this.banned,
banExpires: banExpires ?? this.banExpires,
teams: teams ?? this.teams,
language: language ?? this.language,
);
@@ -186,6 +194,7 @@ class User extends Equatable {
online,
extraData,
banned,
banExpires,
teams,
language,
];
@@ -21,6 +21,9 @@ User _$UserFromJson(Map<String, dynamic> json) => User(
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
online: json['online'] as bool? ?? false,
banned: json['banned'] as bool? ?? false,
banExpires: json['ban_expires'] == null
? null
: DateTime.parse(json['ban_expires'] as String),
teams:
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const [],
@@ -45,7 +48,8 @@ Map<String, dynamic> _$UserToJson(User instance) {
writeNotNull('last_active', readonly(instance.lastActive));
writeNotNull('online', readonly(instance.online));
writeNotNull('banned', readonly(instance.banned));
val['extra_data'] = instance.extraData;
writeNotNull('ban_expires', readonly(instance.banExpires));
writeNotNull('language', instance.language);
val['extra_data'] = instance.extraData;
return val;
}
@@ -253,6 +253,7 @@ abstract class ChatPersistenceClient {
users.addAll([
channel.createdBy,
...messages.map((it) => it.user),
...reads.map((it) => it.user),
...members.map((it) => it.user),
...reactions.map((it) => it.user),
@@ -73,6 +73,12 @@ class EventType {
/// Event sent when a member is removed to a channel
static const String memberRemoved = 'member.removed';
/// Event sent when a member is removed to a channel
static const String userBanned = 'user.banned';
/// Event sent when a member is removed to a channel
static const String userUnbanned = 'user.unbanned';
/// Event sent when a channel is hidden
static const String channelHidden = 'channel.hidden';
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '3.3.1';
const PACKAGE_VERSION = '3.4.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 3.3.1
version: 3.4.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -1,6 +1,6 @@
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/src/core/models/banned_user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -1963,6 +1963,35 @@ void main() {
)).called(1);
});
test('`.queryBannedUsers`', () async {
final filter = Filter.equal('channel_cid', channelCid);
final bans = List.generate(
3,
(index) => BannedUser(
user: User(id: 'test-user-id-$index'),
bannedBy: User(id: 'test-user-id-${index + 1}'),
),
);
when(() => client.queryBannedUsers(
filter: filter,
sort: any(named: 'sort'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans);
final res = await channel.queryBannedUsers();
expect(res, isNotNull);
expect(res.bans.length, bans.length);
verify(() => client.queryBannedUsers(
filter: filter,
sort: any(named: 'sort'),
pagination: any(named: 'pagination'),
)).called(1);
});
test('`.mute`', () async {
when(() => client.muteChannel(
channelCid,
@@ -2046,7 +2075,7 @@ void main() {
{'type': channelType, 'id': channelId, ...options},
)).thenAnswer((_) async => EmptyResponse());
final res = await channel.banUser(userId, options);
final res = await channel.banMember(userId, options);
expect(res, isNotNull);
@@ -2062,7 +2091,7 @@ void main() {
when(() => client.unbanUser(userId, any()))
.thenAnswer((_) async => EmptyResponse());
final res = await channel.unbanUser(userId);
final res = await channel.unbanMember(userId);
expect(res, isNotNull);
@@ -1,20 +1,8 @@
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/client/client.dart';
import 'package:stream_chat/src/core/api/device_api.dart';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/http/token.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/own_user.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/ws/connection_status.dart';
import 'package:stream_chat/src/core/models/banned_user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/scaffolding.dart';
import 'package:test/test.dart';
import '../fakes.dart';
@@ -995,6 +983,36 @@ void main() {
verifyNoMoreInteractions(api.user);
});
test('`.queryBannedUsers`', () async {
final bans = List.generate(
3,
(index) => BannedUser(
user: User(id: 'test-user-id-$index'),
bannedBy: User(id: 'test-user-id-${index + 1}'),
),
);
const cid = 'message:nice-channel';
final filter = Filter.equal('channel_cid', cid);
when(() => api.moderation.queryBannedUsers(
filter: filter,
sort: any(named: 'sort'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans);
final res = await client.queryBannedUsers(filter: filter);
expect(res, isNotNull);
expect(res.bans.length, bans.length);
verify(() => api.moderation.queryBannedUsers(
filter: filter,
sort: any(named: 'sort'),
pagination: any(named: 'pagination'),
)).called(1);
verifyNoMoreInteractions(api.moderation);
});
test('`.search`', () async {
const cid = 'test-type:test-id';
final filter = Filter.in_('cid', const [cid]);
@@ -2314,6 +2332,33 @@ void main() {
verifyNoMoreInteractions(api.message);
});
test('`.enrichUrl`', () async {
const url =
'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example';
when(() => api.general.enrichUrl(url)).thenAnswer(
(_) async => OGAttachmentResponse()
..type = 'image'
..ogScrapeUrl = url
..authorName = 'TechYourChance'
..title = 'Finite State Machine with Unit Tests: Real World Example',
);
final res = await client.enrichUrl(url);
expect(res, isNotNull);
expect(res.type, 'image');
expect(res.ogScrapeUrl, url);
expect(res.authorName, 'TechYourChance');
expect(
res.title,
'Finite State Machine with Unit Tests: Real World Example',
);
verify(() => api.general.enrichUrl(url)).called(1);
verifyNoMoreInteractions(api.general);
});
test(
'''setting the `currentUser` should also compute and update the unreadCounts''',
() {
@@ -4,7 +4,6 @@ import 'package:stream_chat/src/client/retry_queue.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:test/scaffolding.dart';
import 'package:test/test.dart';
import '../mocks.dart';
@@ -3,8 +3,6 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/api/channel_api.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -3,10 +3,6 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/api/general_api.dart';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -281,4 +277,39 @@ void main() {
verifyNoMoreInteractions(client);
});
});
test('enrichUrl', () async {
const path = '/og';
const url =
'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example';
when(() => client.get(
path,
queryParameters: {'url': url},
)).thenAnswer((_) async => successResponse(path, data: {
'type': 'image',
'og_scrape_url': url,
'author_name': 'TechYourChance',
'title': 'Finite State Machine with Unit Tests: Real World Example',
}));
final res = await generalApi.enrichUrl(url);
expect(res, isNotNull);
expect(res.type, 'image');
expect(res.ogScrapeUrl, url);
expect(res.authorName, 'TechYourChance');
expect(
res.title,
'Finite State Machine with Unit Tests: Real World Example',
);
verify(
() => client.get(
path,
queryParameters: {'url': url},
),
).called(1);
verifyNoMoreInteractions(client);
});
}
@@ -1,11 +1,5 @@
import 'dart:convert';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/models/device.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -2,9 +2,7 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/user_api.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -1,12 +1,10 @@
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/location.dart';
import 'package:test/test.dart';
void main() {
test('should return the all default set params', () {
const options = StreamHttpClientOptions();
expect(options.location, isNull);
expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com');
expect(options.baseUrl, 'https://chat.stream-io-api.com');
expect(options.connectTimeout, const Duration(seconds: 6));
expect(options.receiveTimeout, const Duration(seconds: 6));
expect(options.queryParameters, const {});
@@ -21,39 +19,10 @@ void main() {
headers: {'test': 'test'},
queryParameters: {'123': '123'},
);
expect(options.location, isNull);
expect(options.baseUrl, 'base-url');
expect(options.connectTimeout, const Duration(seconds: 3));
expect(options.receiveTimeout, const Duration(seconds: 3));
expect(options.headers, {'test': 'test'});
expect(options.queryParameters, {'123': '123'});
});
group('should create baseUrl according to provided location', () {
test('us-east', () {
const options = StreamHttpClientOptions(location: Location.usEast);
expect(options.location, isNotNull);
expect(options.baseUrl, 'https://chat-proxy-us-east.stream-io-api.com');
});
test('eu-west', () {
const options = StreamHttpClientOptions(location: Location.euWest);
expect(options.location, isNotNull);
expect(options.baseUrl, 'https://chat-proxy-dublin.stream-io-api.com');
});
test('mumbai', () {
const options = StreamHttpClientOptions(location: Location.mumbai);
expect(options.location, isNotNull);
expect(options.baseUrl, 'https://chat-proxy-mumbai.stream-io-api.com');
});
test('sydney', () {
const options = StreamHttpClientOptions(location: Location.sydney);
expect(options.location, isNotNull);
expect(options.baseUrl, 'https://chat-proxy-sydney.stream-io-api.com');
});
test('singapore', () {
const options = StreamHttpClientOptions(location: Location.singapore);
expect(options.location, isNotNull);
expect(options.baseUrl, 'https://chat-proxy-singapore.stream-io-api.com');
});
});
}
@@ -1,8 +1,3 @@
import 'package:stream_chat/src/core/models/channel_config.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/command.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -1,5 +1,3 @@
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/own_user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -1,6 +1,4 @@
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/models/own_user.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
-1
View File
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/core/api/channel_api.dart';