Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into mark-read-fix
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: example
|
||||
name: stream_chat_example
|
||||
description: A new Flutter project.
|
||||
|
||||
publish_to: "none"
|
||||
@@ -11,8 +11,7 @@ dependencies:
|
||||
cupertino_icons: ^1.0.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
stream_chat:
|
||||
path: ../
|
||||
stream_chat: ^2.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -521,7 +521,7 @@ class Channel {
|
||||
state!.addMessage(message);
|
||||
|
||||
try {
|
||||
if (message.attachments.any((it) => !it.uploadState.isSuccess) == true) {
|
||||
if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
|
||||
final attachmentsUploadCompleter = Completer<Message>();
|
||||
_messageAttachmentsUploadCompleter[message.id] =
|
||||
attachmentsUploadCompleter;
|
||||
@@ -849,12 +849,12 @@ class Channel {
|
||||
..update(type, (value) {
|
||||
if (enforceUnique) return value;
|
||||
return value + 1;
|
||||
}, ifAbsent: () => 1),
|
||||
}, ifAbsent: () => 1), // ignore: prefer-trailing-comma
|
||||
reactionScores: {...message.reactionScores ?? <String, int>{}}
|
||||
..update(type, (value) {
|
||||
if (enforceUnique) return value;
|
||||
return value + 1;
|
||||
}, ifAbsent: () => 1),
|
||||
}, ifAbsent: () => 1), // ignore: prefer-trailing-comma
|
||||
latestReactions: latestReactions,
|
||||
ownReactions: ownReactions,
|
||||
);
|
||||
@@ -878,7 +878,9 @@ class Channel {
|
||||
|
||||
/// Delete a reaction from this channel.
|
||||
Future<EmptyResponse> deleteReaction(
|
||||
Message message, Reaction reaction) async {
|
||||
Message message,
|
||||
Reaction reaction,
|
||||
) async {
|
||||
final type = reaction.type;
|
||||
final user = _client.state.currentUser;
|
||||
|
||||
@@ -1336,7 +1338,7 @@ class Channel {
|
||||
type,
|
||||
clearHistory: clearHistory,
|
||||
);
|
||||
if (clearHistory == true) {
|
||||
if (clearHistory) {
|
||||
state!.truncate();
|
||||
final cid = _cid;
|
||||
if (cid != null) {
|
||||
@@ -1501,28 +1503,27 @@ class ChannelClientState {
|
||||
final expiredAttachmentMessagesId = channelState.messages
|
||||
.where((m) =>
|
||||
!_updatedMessagesIds.contains(m.id) &&
|
||||
m.attachments.isNotEmpty == true &&
|
||||
m.attachments.isNotEmpty &&
|
||||
m.attachments.any((e) {
|
||||
final url = e.imageUrl ?? e.assetUrl;
|
||||
if (url == null || !url.contains('')) {
|
||||
return false;
|
||||
}
|
||||
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());
|
||||
}) ==
|
||||
true)
|
||||
final url = e.imageUrl ?? e.assetUrl;
|
||||
if (url == null || !url.contains('')) {
|
||||
return false;
|
||||
}
|
||||
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());
|
||||
}))
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
|
||||
if (expiredAttachmentMessagesId.isNotEmpty == true) {
|
||||
if (expiredAttachmentMessagesId.isNotEmpty) {
|
||||
await _channel._initializedCompleter.future;
|
||||
_updatedMessagesIds.addAll(expiredAttachmentMessagesId);
|
||||
_channel.getMessagesById(expiredAttachmentMessagesId);
|
||||
@@ -1546,7 +1547,8 @@ class ChannelClientState {
|
||||
final user = e.user;
|
||||
updateChannelState(channelState.copyWith(
|
||||
members: List.from(
|
||||
channelState.members..removeWhere((m) => m.userId == user!.id)),
|
||||
channelState.members..removeWhere((m) => m.userId == user!.id),
|
||||
),
|
||||
));
|
||||
}));
|
||||
}
|
||||
@@ -1647,7 +1649,7 @@ class ChannelClientState {
|
||||
);
|
||||
addMessage(message);
|
||||
|
||||
if (message.pinned == true) {
|
||||
if (message.pinned) {
|
||||
_channelState = _channelState.copyWith(
|
||||
pinnedMessages: [
|
||||
..._channelState.pinnedMessages,
|
||||
@@ -1794,9 +1796,8 @@ class ChannelClientState {
|
||||
channelStateStream.map((cs) => cs.pinnedMessages.toList());
|
||||
|
||||
/// Get channel last message.
|
||||
Message? get lastMessage => _channelState.messages.isNotEmpty == true
|
||||
? _channelState.messages.last
|
||||
: null;
|
||||
Message? get lastMessage =>
|
||||
_channelState.messages.isNotEmpty ? _channelState.messages.last : null;
|
||||
|
||||
/// Get channel last message.
|
||||
Stream<Message?> get lastMessageStream =>
|
||||
@@ -1859,8 +1860,8 @@ class ChannelClientState {
|
||||
(m) => m.user.id == message.user?.id,
|
||||
) !=
|
||||
null;
|
||||
return message.silent != true &&
|
||||
message.shadowed != true &&
|
||||
return !message.silent &&
|
||||
!message.shadowed &&
|
||||
message.user?.id != userId &&
|
||||
!userIsMuted;
|
||||
}
|
||||
@@ -1898,9 +1899,7 @@ class ChannelClientState {
|
||||
...updatedState.messages,
|
||||
..._channelState.messages
|
||||
.where((m) =>
|
||||
updatedState.messages
|
||||
.any((newMessage) => newMessage.id == m.id) !=
|
||||
true)
|
||||
!updatedState.messages.any((newMessage) => newMessage.id == m.id))
|
||||
.toList(),
|
||||
]..sort(_sortByCreatedAt);
|
||||
|
||||
@@ -1908,9 +1907,7 @@ class ChannelClientState {
|
||||
...updatedState.watchers,
|
||||
..._channelState.watchers
|
||||
.where((w) =>
|
||||
updatedState.watchers
|
||||
.any((newWatcher) => newWatcher.id == w.id) !=
|
||||
true)
|
||||
!updatedState.watchers.any((newWatcher) => newWatcher.id == w.id))
|
||||
.toList(),
|
||||
];
|
||||
|
||||
@@ -1922,9 +1919,7 @@ class ChannelClientState {
|
||||
...updatedState.read,
|
||||
..._channelState.read
|
||||
.where((r) =>
|
||||
updatedState.read
|
||||
.any((newRead) => newRead.user.id == r.user.id) !=
|
||||
true)
|
||||
!updatedState.read.any((newRead) => newRead.user.id == r.user.id))
|
||||
.toList(),
|
||||
];
|
||||
|
||||
@@ -2031,7 +2026,7 @@ class ChannelClientState {
|
||||
.on()
|
||||
.where((event) =>
|
||||
event.user != null &&
|
||||
members.any((m) => m.userId == event.user!.id) == true)
|
||||
members.any((m) => m.userId == event.user!.id))
|
||||
.listen(
|
||||
(event) {
|
||||
final newMembers = List<Member>.from(members);
|
||||
|
||||
@@ -75,7 +75,7 @@ class ChannelApi {
|
||||
if (messageLimit != null) 'message_limit': messageLimit,
|
||||
|
||||
// pagination
|
||||
...paginationParams.toJson()
|
||||
...paginationParams.toJson(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ enum PushProvider {
|
||||
firebase,
|
||||
|
||||
/// Send notifications using Apple's Push Notification service
|
||||
apn
|
||||
apn,
|
||||
}
|
||||
|
||||
/// Helper extension for [PushProvider]
|
||||
|
||||
@@ -90,7 +90,7 @@ enum ChatErrorCode {
|
||||
internalSystemError,
|
||||
|
||||
/// No access to requested channels
|
||||
noAccessToChannels
|
||||
noAccessToChannels,
|
||||
}
|
||||
|
||||
const _errorCodeWithDescription = {
|
||||
@@ -98,14 +98,20 @@ const _errorCodeWithDescription = {
|
||||
MapEntry(1000, 'Unauthorised, token not defined'),
|
||||
ChatErrorCode.inputError:
|
||||
MapEntry(4, 'Wrong data/parameter is sent to the API'),
|
||||
ChatErrorCode.duplicateUsername: MapEntry(6,
|
||||
'Duplicate username is sent while enforce_unique_usernames is enabled'),
|
||||
ChatErrorCode.duplicateUsername: MapEntry(
|
||||
6,
|
||||
'Duplicate username is sent while enforce_unique_usernames is enabled',
|
||||
),
|
||||
ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'),
|
||||
ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'),
|
||||
ChatErrorCode.channelFeatureNotSupported: MapEntry(19,
|
||||
'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)'),
|
||||
ChatErrorCode.multipleNestling: MapEntry(21,
|
||||
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads'),
|
||||
ChatErrorCode.channelFeatureNotSupported: MapEntry(
|
||||
19,
|
||||
'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)',
|
||||
),
|
||||
ChatErrorCode.multipleNestling: MapEntry(
|
||||
21,
|
||||
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads',
|
||||
),
|
||||
ChatErrorCode.customCommandEndpointCall:
|
||||
MapEntry(45, 'Custom Command handler returned an error'),
|
||||
ChatErrorCode.customCommandEndpointMissing:
|
||||
|
||||
@@ -35,7 +35,8 @@ class StreamWebSocketError extends StreamChatError {
|
||||
|
||||
///
|
||||
factory StreamWebSocketError.fromWebSocketChannelError(
|
||||
WebSocketChannelException error) {
|
||||
WebSocketChannelException error,
|
||||
) {
|
||||
final message = error.message ?? '';
|
||||
return StreamWebSocketError(message);
|
||||
}
|
||||
|
||||
@@ -105,8 +105,11 @@ class LoggingInterceptor extends Interceptor {
|
||||
final formDataMap = <String, dynamic>{}
|
||||
..addEntries(data.fields)
|
||||
..addEntries(data.files);
|
||||
_printMapAsTable(_logPrintRequest, formDataMap,
|
||||
header: 'Form data | ${data.boundary}');
|
||||
_printMapAsTable(
|
||||
_logPrintRequest,
|
||||
formDataMap,
|
||||
header: 'Form data | ${data.boundary}',
|
||||
);
|
||||
} else {
|
||||
_printBlock(_logPrintRequest, data.toString());
|
||||
}
|
||||
@@ -201,14 +204,19 @@ class LoggingInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
void _printRequestHeader(
|
||||
void Function(Object) logPrint, RequestOptions options) {
|
||||
void Function(Object) logPrint,
|
||||
RequestOptions options,
|
||||
) {
|
||||
final uri = options.uri;
|
||||
final method = options.method;
|
||||
_printBoxed(logPrint, header: 'Request ║ $method ', text: uri.toString());
|
||||
}
|
||||
|
||||
void _printLine(void Function(Object) logPrint,
|
||||
[String pre = '', String suf = '╝']) =>
|
||||
void _printLine(
|
||||
void Function(Object) logPrint, [
|
||||
String pre = '',
|
||||
String suf = '╝',
|
||||
]) =>
|
||||
logPrint('$pre${'═' * maxWidth}$suf');
|
||||
|
||||
void _printKV(void Function(Object) logPrint, String? key, Object? v) {
|
||||
@@ -227,8 +235,10 @@ class LoggingInterceptor extends Interceptor {
|
||||
final lines = (msg.length / maxWidth).ceil();
|
||||
for (var i = 0; i < lines; ++i) {
|
||||
logPrint((i >= 0 ? '║ ' : '') +
|
||||
msg.substring(i * maxWidth,
|
||||
math.min<int>(i * maxWidth + maxWidth, msg.length)));
|
||||
msg.substring(
|
||||
i * maxWidth,
|
||||
math.min<int>(i * maxWidth + maxWidth, msg.length),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,8 +311,13 @@ class LoggingInterceptor extends Interceptor {
|
||||
if (compact) {
|
||||
logPrint('║${_indent(tabs)} $e${!isLast ? ',' : ''}');
|
||||
} else {
|
||||
_printPrettyMap(logPrint, e,
|
||||
tabs: tabs + 1, isListItem: true, isLast: isLast);
|
||||
_printPrettyMap(
|
||||
logPrint,
|
||||
e,
|
||||
tabs: tabs + 1,
|
||||
isListItem: true,
|
||||
isLast: isLast,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logPrint('║${_indent(tabs + 2)} $e${isLast ? '' : ','}');
|
||||
|
||||
@@ -56,12 +56,15 @@ class Attachment extends Equatable {
|
||||
/// Create a new instance from a json
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// Create a new instance from a db data
|
||||
factory Attachment.fromData(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(Serializer.moveToExtraDataFromRoot(
|
||||
json, topLevelFields + dbSpecificTopLevelFields));
|
||||
json,
|
||||
topLevelFields + dbSpecificTopLevelFields,
|
||||
));
|
||||
|
||||
///The attachment type based on the URL resource. This can be: audio,
|
||||
///image or video
|
||||
|
||||
@@ -11,54 +11,6 @@ part 'attachment_file.freezed.dart';
|
||||
|
||||
part 'attachment_file.g.dart';
|
||||
|
||||
/// Union class to hold various [UploadState] of a attachment.
|
||||
@freezed
|
||||
class UploadState with _$UploadState {
|
||||
/// Preparing state of the union
|
||||
const factory UploadState.preparing() = Preparing;
|
||||
|
||||
/// InProgress state of the union
|
||||
const factory UploadState.inProgress({
|
||||
required int uploaded,
|
||||
required int total,
|
||||
}) = InProgress;
|
||||
|
||||
/// Success state of the union
|
||||
const factory UploadState.success() = Success;
|
||||
|
||||
/// Failed state of the union
|
||||
const factory UploadState.failed({required String error}) = Failed;
|
||||
|
||||
/// Creates a new instance from a json
|
||||
factory UploadState.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadStateFromJson(json);
|
||||
}
|
||||
|
||||
/// Helper extension for UploadState
|
||||
extension UploadStateX on UploadState? {
|
||||
/// Returns true if state is [Preparing]
|
||||
bool get isPreparing => this is Preparing;
|
||||
|
||||
/// Returns true if state is [InProgress]
|
||||
bool get isInProgress => this is InProgress;
|
||||
|
||||
/// Returns true if state is [Success]
|
||||
bool get isSuccess => this is Success;
|
||||
|
||||
/// Returns true if state is [Failed]
|
||||
bool get isFailed => this is Failed;
|
||||
}
|
||||
|
||||
Uint8List? _fromString(String? bytes) {
|
||||
if (bytes == null) return null;
|
||||
return Uint8List.fromList(bytes.codeUnits);
|
||||
}
|
||||
|
||||
String? _toString(Uint8List? bytes) {
|
||||
if (bytes == null) return null;
|
||||
return String.fromCharCodes(bytes);
|
||||
}
|
||||
|
||||
/// The class that contains the information about an attachment file
|
||||
@JsonSerializable()
|
||||
class AttachmentFile {
|
||||
@@ -135,3 +87,51 @@ class AttachmentFile {
|
||||
return multiPartFile;
|
||||
}
|
||||
}
|
||||
|
||||
/// Union class to hold various [UploadState] of a attachment.
|
||||
@freezed
|
||||
class UploadState with _$UploadState {
|
||||
/// Preparing state of the union
|
||||
const factory UploadState.preparing() = Preparing;
|
||||
|
||||
/// InProgress state of the union
|
||||
const factory UploadState.inProgress({
|
||||
required int uploaded,
|
||||
required int total,
|
||||
}) = InProgress;
|
||||
|
||||
/// Success state of the union
|
||||
const factory UploadState.success() = Success;
|
||||
|
||||
/// Failed state of the union
|
||||
const factory UploadState.failed({required String error}) = Failed;
|
||||
|
||||
/// Creates a new instance from a json
|
||||
factory UploadState.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadStateFromJson(json);
|
||||
}
|
||||
|
||||
/// Helper extension for UploadState
|
||||
extension UploadStateX on UploadState? {
|
||||
/// Returns true if state is [Preparing]
|
||||
bool get isPreparing => this is Preparing;
|
||||
|
||||
/// Returns true if state is [InProgress]
|
||||
bool get isInProgress => this is InProgress;
|
||||
|
||||
/// Returns true if state is [Success]
|
||||
bool get isSuccess => this is Success;
|
||||
|
||||
/// Returns true if state is [Failed]
|
||||
bool get isFailed => this is Failed;
|
||||
}
|
||||
|
||||
Uint8List? _fromString(String? bytes) {
|
||||
if (bytes == null) return null;
|
||||
return Uint8List.fromList(bytes.codeUnits);
|
||||
}
|
||||
|
||||
String? _toString(Uint8List? bytes) {
|
||||
if (bytes == null) return null;
|
||||
return String.fromCharCodes(bytes);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ class ChannelModel {
|
||||
/// Create a new instance from a json
|
||||
factory ChannelModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelModelFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// The id of this channel
|
||||
final String id;
|
||||
|
||||
@@ -81,7 +81,8 @@ class Message extends Equatable {
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// The message ID. This is either created by Stream or set client side when
|
||||
/// the message is added.
|
||||
|
||||
@@ -48,7 +48,8 @@ class OwnUser extends User {
|
||||
|
||||
/// Create a new instance from json.
|
||||
factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// Create a new instance from [User] object.
|
||||
factory OwnUser.fromUser(User user) => OwnUser(
|
||||
|
||||
@@ -27,7 +27,8 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.0.1
|
||||
dart_code_metrics: ^4.4.0
|
||||
freezed: ^0.14.1+3
|
||||
json_serializable: ^5.0.2
|
||||
mocktail: ^0.1.1
|
||||
test: ^1.18.2
|
||||
test: ^1.17.12
|
||||
@@ -291,7 +291,7 @@ void main() {
|
||||
(index) => Attachment(
|
||||
id: 'test-attachment-id-$index',
|
||||
type: index.isEven ? 'image' : 'file',
|
||||
file: AttachmentFile(size: 33 * index, path: 'test-file-path'),
|
||||
file: AttachmentFile(size: index * 33, path: 'test-file-path'),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -498,7 +498,7 @@ void main() {
|
||||
(index) => Attachment(
|
||||
id: 'test-attachment-id-$index',
|
||||
type: index.isEven ? 'image' : 'file',
|
||||
file: AttachmentFile(size: 33 * index, path: 'test-file-path'),
|
||||
file: AttachmentFile(size: index * 33, path: 'test-file-path'),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user