Merge remote-tracking branch 'origin/v7.0.0'

This commit is contained in:
Efthymis Sarmpanis
2023-12-04 12:16:34 +02:00
188 changed files with 4662 additions and 4119 deletions
+23
View File
@@ -14,6 +14,11 @@
- [[#1724]](https://github.com/GetStream/stream-chat-flutter/issues/1724) Fixed sendFile error
on `AttachmentFile` with `bytes` and no `name`.
## 7.0.0-beta.3
- Included the changes from version [6.8.0](#670).
- Updated minimum supported `SDK` version to Dart 3.1
## 6.8.0
🐞 Fixed
@@ -29,6 +34,10 @@
- Updated minimum supported `SDK` version to Dart 3.0
## 7.0.0-beta.2
- Included the changes from version [6.7.0](#670).
## 6.7.0
✅ Added
@@ -41,6 +50,20 @@
should have the Create System Message permission. Server-side system messages don't need that permission.
```
## 7.0.0-beta.1
🛑️ Breaking
- Removed deprecated `channelQuery.sort` property. Use `channelStateSort` instead.
- Removed deprecated `RetryPolicy.retryTimeout` property. Use `delayFactor` instead.
- Removed deprecated `StreamChatNetworkError.fromDioError` constructor. Use `StreamChatNetworkError.fromDioException`
instead.
- Removed deprecated `MessageSendingStatus` enum. Use `MessageState` instead.
🔄 Changed
- Updated minimum supported `SDK` version to Dart 3.0
## 6.6.0
🔄 Changed
+2 -2
View File
@@ -5,8 +5,8 @@ publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.10.0"
sdk: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"
dependencies:
cupertino_icons: ^1.0.5
@@ -499,7 +499,7 @@ class Channel {
]);
}
final isImage = it.type == 'image';
final isImage = it.type == AttachmentType.image;
final cancelToken = CancelToken();
Future<SendAttachmentResponse> future;
if (isImage) {
@@ -17,7 +17,6 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/http/token.dart';
import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/core/models/attachment_file.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/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
@@ -572,8 +571,6 @@ class StreamChatClient {
/// Requests channels with a given query.
Stream<List<Channel>> queryChannels({
Filter? filter,
@Deprecated('Use channelStateSort instead.')
List<SortOption<ChannelModel>>? sort,
List<SortOption<ChannelState>>? channelStateSort,
bool state = true,
bool watch = true,
@@ -590,7 +587,7 @@ class StreamChatClient {
final hash = generateHash([
filter,
sort,
channelStateSort,
state,
watch,
presence,
@@ -604,8 +601,6 @@ class StreamChatClient {
} else {
final channels = await queryChannelsOffline(
filter: filter,
// ignore: deprecated_member_use_from_same_package
sort: sort,
channelStateSort: channelStateSort,
paginationParams: paginationParams,
);
@@ -614,7 +609,7 @@ class StreamChatClient {
try {
final newQueryChannelsFuture = queryChannelsOnline(
filter: filter,
sort: channelStateSort ?? sort,
sort: channelStateSort,
state: state,
watch: watch,
presence: presence,
@@ -731,17 +726,11 @@ class StreamChatClient {
/// Requests channels with a given query from the Persistence client.
Future<List<Channel>> queryChannelsOffline({
Filter? filter,
@Deprecated('''
sort has been deprecated.
Please use channelStateSort instead.''')
List<SortOption<ChannelModel>>? sort,
List<SortOption<ChannelState>>? channelStateSort,
PaginationParams paginationParams = const PaginationParams(),
}) async {
final offlineChannels = (await chatPersistenceClient?.getChannelStates(
filter: filter,
// ignore: deprecated_member_use_from_same_package
sort: sort,
channelStateSort: channelStateSort,
paginationParams: paginationParams,
)) ??
@@ -13,7 +13,6 @@ class RetryPolicy {
/// Instantiate a new RetryPolicy
RetryPolicy({
required this.shouldRetry,
@Deprecated("Use 'delayFactor' instead.") this.retryTimeout,
this.maxRetryAttempts = 6,
this.delayFactor = const Duration(milliseconds: 200),
this.randomizationFactor = 0.25,
@@ -53,13 +52,4 @@ class RetryPolicy {
int attempt,
StreamChatError? error,
) shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout
/// method is called to determine the timeout
@Deprecated("Use 'delayFactor' instead.")
final Duration Function(
StreamChatClient client,
int attempt,
StreamChatError? error,
)? retryTimeout;
}
@@ -1,5 +1,3 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -88,11 +88,6 @@ class StreamChatNetworkError extends StreamChatError {
this.isRequestCancelledError = false,
}) : super(message);
///
@Deprecated('Use `StreamChatNetworkError.fromDioException` instead')
factory StreamChatNetworkError.fromDioError(DioException error) =
StreamChatNetworkError.fromDioException;
///
factory StreamChatNetworkError.fromDioException(DioException exception) {
final response = exception.response;
@@ -10,13 +10,25 @@ import 'package:uuid/uuid.dart';
part 'attachment.g.dart';
mixin AttachmentType {
/// Backend specified types.
static const image = 'image';
static const file = 'file';
static const giphy = 'giphy';
static const video = 'video';
static const audio = 'audio';
/// Application custom types.
static const urlPreview = 'url_preview';
}
/// The class that contains the information about an attachment
@JsonSerializable(includeIfNull: false)
class Attachment extends Equatable {
/// Constructor used for json serialization
Attachment({
String? id,
this.type,
String? type,
this.titleLink,
String? title,
this.thumbUrl,
@@ -33,26 +45,24 @@ class Attachment extends Equatable {
this.authorLink,
this.authorIcon,
this.assetUrl,
List<Action>? actions,
this.actions = const [],
this.originalWidth,
this.originalHeight,
Map<String, Object?> extraData = const {},
this.file,
UploadState? uploadState,
}) : id = id ?? const Uuid().v4(),
_type = type,
title = title ?? file?.name,
_uploadState = uploadState,
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
actions = actions ?? [],
// For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData].
extraData = {
...extraData,
if (file?.size != null) 'file_size': file?.size,
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
} {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType,
};
/// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) =>
@@ -69,7 +79,8 @@ class Attachment extends Equatable {
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
Attachment(
type: ogAttachment.type,
// If the type is not specified, we default to urlPreview.
type: ogAttachment.type ?? AttachmentType.urlPreview,
title: ogAttachment.title,
titleLink: ogAttachment.titleLink,
text: ogAttachment.text,
@@ -84,7 +95,20 @@ class Attachment extends Equatable {
///The attachment type based on the URL resource. This can be: audio,
///image or video
final String? type;
String? get type {
// If the attachment contains titleLink but is not of type giphy, we
// consider it as a urlPreview.
if (_type != AttachmentType.giphy && titleLink != null) {
return AttachmentType.urlPreview;
}
return _type;
}
final String? _type;
/// The raw attachment type.
String? get rawType => _type;
///The link to which the attachment message points to.
final String? titleLink;
@@ -126,13 +150,27 @@ class Attachment extends Equatable {
/// Actions from a command
final List<Action>? actions;
/// The original width of the attached image.
final int? originalWidth;
/// The original height of the attached image.
final int? originalHeight;
final Uri? localUri;
/// The file present inside this attachment.
final AttachmentFile? file;
/// The current upload state of the attachment
late final UploadState uploadState;
UploadState get uploadState {
if (_uploadState case final state?) return state;
return ((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
final UploadState? _uploadState;
/// Map of custom channel extraData
final Map<String, Object?> extraData;
@@ -175,6 +213,8 @@ class Attachment extends Equatable {
'author_icon',
'asset_url',
'actions',
'original_width',
'original_height',
];
/// Known db specific top level fields.
@@ -214,6 +254,8 @@ class Attachment extends Equatable {
String? authorIcon,
String? assetUrl,
List<Action>? actions,
int? originalWidth,
int? originalHeight,
AttachmentFile? file,
UploadState? uploadState,
Map<String, Object?>? extraData,
@@ -238,6 +280,8 @@ class Attachment extends Equatable {
authorIcon: authorIcon ?? this.authorIcon,
assetUrl: assetUrl ?? this.assetUrl,
actions: actions ?? this.actions,
originalWidth: originalWidth ?? this.originalWidth,
originalHeight: originalHeight ?? this.originalHeight,
file: file ?? this.file,
uploadState: uploadState ?? this.uploadState,
extraData: extraData ?? this.extraData,
@@ -264,6 +308,8 @@ class Attachment extends Equatable {
authorIcon: other.authorIcon,
assetUrl: other.assetUrl,
actions: other.actions,
originalWidth: other.originalWidth,
originalHeight: other.originalHeight,
file: other.file,
uploadState: other.uploadState,
extraData: other.extraData,
@@ -291,6 +337,8 @@ class Attachment extends Equatable {
authorIcon,
assetUrl,
actions,
originalWidth,
originalHeight,
file,
uploadState,
extraData,
@@ -26,8 +26,11 @@ Attachment _$AttachmentFromJson(Map<String, dynamic> json) => Attachment(
authorIcon: json['author_icon'] as String?,
assetUrl: json['asset_url'] as String?,
actions: (json['actions'] as List<dynamic>?)
?.map((e) => Action.fromJson(e as Map<String, dynamic>))
.toList(),
?.map((e) => Action.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
originalWidth: json['original_width'] as int?,
originalHeight: json['original_height'] as int?,
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
file: json['file'] == null
? null
@@ -64,6 +67,8 @@ Map<String, dynamic> _$AttachmentToJson(Attachment instance) {
writeNotNull('author_icon', instance.authorIcon);
writeNotNull('asset_url', instance.assetUrl);
writeNotNull('actions', instance.actions?.map((e) => e.toJson()).toList());
writeNotNull('original_width', instance.originalWidth);
writeNotNull('original_height', instance.originalHeight);
writeNotNull('file', instance.file?.toJson());
val['upload_state'] = instance.uploadState.toJson();
val['extra_data'] = instance.extraData;
@@ -62,7 +62,7 @@ class AttachmentFile {
String? get extension => name?.split('.').last;
/// The mime type of this file.
MediaType? get mimeType => name?.mimeType;
MediaType? get mediaType => name?.mediaType;
/// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
@@ -74,14 +74,14 @@ class AttachmentFile {
if (CurrentPlatform.isWeb) {
multiPartFile = MultipartFile.fromBytes(
bytes!,
filename: name ?? 'file',
contentType: mimeType,
filename: name,
contentType: mediaType,
);
} else {
multiPartFile = await MultipartFile.fromFile(
path!,
filename: name ?? 'file',
contentType: mimeType,
filename: name,
contentType: mediaType,
);
}
return multiPartFile;
@@ -0,0 +1,69 @@
import 'package:stream_chat/src/core/models/attachment.dart';
/// {@template giphy_info_type}
/// The different types of quality for a Giphy attachment.
/// {@endtemplate}
enum GiphyInfoType {
/// Original quality giphy, the largest size to load.
original('original'),
/// Lower quality with a fixed height, adjusts width according to the
/// Giphy aspect ratio. Lower size than [original].
fixedHeight('fixed_height'),
/// Still image of the [fixedHeight] giphy.
fixedHeightStill('fixed_height_still'),
/// Lower quality with a fixed height with width adjusted according to the
/// aspect ratio and played at a lower frame rate. Significantly lower size,
/// but visually less appealing.
fixedHeightDownsampled('fixed_height_downsampled');
/// {@macro giphy_info_type}
const GiphyInfoType(this.value);
/// The value of the [GiphyInfoType].
final String value;
}
/// {@template giphy_info}
/// A class that contains extra information about a Giphy attachment.
/// {@endtemplate}
class GiphyInfo {
/// {@macro giphy_info}
const GiphyInfo({
required this.url,
required this.width,
required this.height,
});
/// The url for the Giphy image.
final String url;
/// The width of the Giphy image.
final double width;
/// The height of the Giphy image.
final double height;
@override
String toString() => 'GiphyInfo{url: $url, width: $width, height: $height}';
}
/// GiphyInfo extension on [Attachment] class.
extension GiphyInfoX on Attachment {
/// Returns the [GiphyInfo] for the given [type].
GiphyInfo? giphyInfo(GiphyInfoType type) {
final giphy = extraData['giphy'] as Map<String, Object?>?;
if (giphy == null) return null;
final info = giphy[type.value] as Map<String, Object?>?;
if (info == null) return null;
return GiphyInfo(
url: info['url']! as String,
width: double.parse(info['width']! as String),
height: double.parse(info['height']! as String),
);
}
}
@@ -1,5 +1,3 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/user.dart';
@@ -15,70 +15,6 @@ class _NullConst {
const _nullConst = _NullConst();
/// Enum defining the status of a sending message.
enum MessageSendingStatus {
/// Message is being sent
sending,
/// Message is being updated
updating,
/// Message is being deleted
deleting,
/// Message failed to send
failed,
/// Message failed to updated
// ignore: constant_identifier_names
failed_update,
/// Message failed to delete
// ignore: constant_identifier_names
failed_delete,
/// Message correctly sent
sent;
/// Returns a [MessageState] from a [MessageSendingStatus]
MessageState toMessageState() {
switch (this) {
case MessageSendingStatus.sending:
return MessageState.sending;
case MessageSendingStatus.updating:
return MessageState.updating;
case MessageSendingStatus.deleting:
return MessageState.softDeleting;
case MessageSendingStatus.failed:
return MessageState.sendingFailed;
case MessageSendingStatus.failed_update:
return MessageState.updatingFailed;
case MessageSendingStatus.failed_delete:
return MessageState.softDeletingFailed;
case MessageSendingStatus.sent:
return MessageState.sent;
}
}
/// Returns a [MessageSendingStatus] from a [MessageState].
static MessageSendingStatus fromMessageState(MessageState state) {
return state.when(
initial: () => MessageSendingStatus.sending,
outgoing: (it) => it.when(
sending: () => MessageSendingStatus.sending,
updating: () => MessageSendingStatus.updating,
deleting: (_) => MessageSendingStatus.deleting,
),
completed: (_) => MessageSendingStatus.sent,
failed: (it, __) => it.when(
sendingFailed: () => MessageSendingStatus.failed,
updatingFailed: () => MessageSendingStatus.failed_update,
deletingFailed: (_) => MessageSendingStatus.failed_delete,
),
);
}
}
/// The class that contains the information about a message.
@JsonSerializable()
class Message extends Equatable {
@@ -114,23 +50,14 @@ class Message extends Equatable {
DateTime? pinExpires,
this.pinnedBy,
this.extraData = const {},
@Deprecated('Use `state` instead') MessageSendingStatus? status,
MessageState? state,
this.state = const MessageState.initial(),
this.i18n,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
remoteCreatedAt = createdAt,
remoteUpdatedAt = updatedAt,
remoteDeletedAt = deletedAt,
_quotedMessageId = quotedMessageId {
var messageState = state ?? const MessageState.initial();
// Backward compatibility. TODO: Remove in the next major version
if (status != null) {
messageState = status.toMessageState();
}
this.state = messageState;
}
_quotedMessageId = quotedMessageId;
/// Create a new instance from JSON.
factory Message.fromJson(Map<String, dynamic> json) {
@@ -155,17 +82,9 @@ class Message extends Equatable {
/// The text of this message.
final String? text;
/// The status of a sending message.
@Deprecated('Use `state` instead')
@JsonKey(includeFromJson: false, includeToJson: false)
MessageSendingStatus get status {
return MessageSendingStatus.fromMessageState(state);
}
// TODO: Remove late modifier in the next major version.
/// The current state of the message.
@JsonKey(includeFromJson: false, includeToJson: false)
late final MessageState state;
final MessageState state;
/// The message type.
@JsonKey(includeIfNull: false, toJson: _typeToJson)
@@ -304,6 +223,9 @@ class Message extends Equatable {
/// Message custom extraData.
final Map<String, Object?> extraData;
/// True if the message is a error.
bool get isError => type == 'error';
/// True if the message is a system info.
bool get isSystem => type == 'system';
@@ -388,7 +310,6 @@ class Message extends Equatable {
Object? pinExpires = _nullConst,
User? pinnedBy,
Map<String, Object?>? extraData,
@Deprecated('Use `state` instead') MessageSendingStatus? status,
MessageState? state,
Map<String, String>? i18n,
}) {
@@ -423,8 +344,6 @@ class Message extends Equatable {
return true;
}(), 'Validate type for quotedMessage');
final messageState = state ?? status?.toMessageState();
return Message(
id: id ?? this.id,
text: text ?? this.text,
@@ -461,7 +380,7 @@ class Message extends Equatable {
pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?,
pinnedBy: pinnedBy ?? this.pinnedBy,
extraData: extraData ?? this.extraData,
state: messageState ?? this.state,
state: state ?? this.state,
i18n: i18n ?? this.i18n,
);
}
@@ -20,8 +20,8 @@ extension MapX<K, V> on Map<K?, V?> {
/// Useful extension functions for [String]
extension StringX on String {
/// returns the mime type from the passed file name.
MediaType? get mimeType {
/// returns the media type from the passed file name.
MediaType? get mediaType {
if (toLowerCase().endsWith('heic')) {
return MediaType.parse('image/heic');
} else {
@@ -102,8 +102,6 @@ abstract class ChatPersistenceClient {
/// for filtering out states.
Future<List<ChannelState>> getChannelStates({
Filter? filter,
@Deprecated('Use channelStateSort instead.')
List<SortOption<ChannelModel>>? sort,
List<SortOption<ChannelState>>? channelStateSort,
PaginationParams? paginationParams,
});
@@ -28,6 +28,7 @@ export 'src/core/http/interceptor/logging_interceptor.dart';
export 'src/core/models/action.dart';
export 'src/core/models/attachment.dart';
export 'src/core/models/attachment_file.dart';
export 'src/core/models/attachment_giphy_info.dart';
export 'src/core/models/channel_config.dart';
export 'src/core/models/channel_model.dart';
export 'src/core/models/channel_mute.dart';
+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 = '6.10.0';
const PACKAGE_VERSION = '7.0.0';
+3 -3
View File
@@ -1,16 +1,16 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 6.10.0
version: 7.0.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: '>=3.1.0 <4.0.0'
dependencies:
async: ^2.11.0
collection: ^1.17.1
collection: ^1.17.2
dio: ^5.3.2
equatable: ^2.0.5
freezed_annotation: ^2.4.1
+1 -1
View File
@@ -5,7 +5,7 @@
"silent": false,
"attachments": [
{
"type": "video",
"type": "giphy",
"title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif",
"title": "The Lion King Disney GIF - Find & Share on GIPHY",
"thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif",
@@ -805,7 +805,7 @@ void main() {
emits(ConnectionStatus.disconnected),
);
await client.disconnectUser();
await client.disconnectUser(flushChatPersistence: true);
expect(client.state.currentUser, isNull);
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
@@ -27,7 +27,7 @@ void main() {
test('should serialize to json correctly', () {
final channel = Attachment(
type: 'image',
type: 'giphy',
title: 'soo',
titleLink:
'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti',
@@ -36,7 +36,7 @@ void main() {
expect(
channel.toJson(),
{
'type': 'image',
'type': 'giphy',
'title': 'soo',
'title_link':
'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti',
@@ -38,7 +38,7 @@ void main() {
'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA',
attachments: [
Attachment.fromJson(const {
'type': 'video',
'type': 'giphy',
'author_name': 'GIPHY',
'title': 'The Lion King Disney GIF - Find \u0026 Share on GIPHY',
'title_link':
@@ -25,13 +25,13 @@ void main() {
group('mimeType', () {
test('should return null if `String` is not a filename', () {
const fileName = 'not-a-file-name';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNull);
});
test('should return mimeType if string is a filename', () {
const fileName = 'dummyFileName.jpeg';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'jpeg');
@@ -39,7 +39,7 @@ void main() {
test('should return `image/heic` if ends with `heic`', () {
const fileName = 'dummyFileName.heic';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'heic');
@@ -62,8 +62,6 @@ class TestPersistenceClient extends ChatPersistenceClient {
@override
Future<List<ChannelState>> getChannelStates(
{Filter? filter,
@Deprecated('Use channelStateSort instead.')
List<SortOption<ChannelModel>>? sort,
List<SortOption<ChannelState>>? channelStateSort,
PaginationParams? paginationParams}) =>
throw UnimplementedError();