Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into feat/capabilities
Conflicts: packages/stream_chat/CHANGELOG.md packages/stream_chat_flutter/CHANGELOG.md
This commit is contained in:
@@ -2,12 +2,17 @@
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `client.enrichUrl` endpoint for enriching URLs with metadata.
|
||||
- Fixed `unreadCount` after removing user from a channel.
|
||||
- `ChannelModel` now supplies individual user capabilities.
|
||||
|
||||
## 3.3.1
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#799]](https://github.com/GetStream/stream-chat-flutter/issues/799) Fixed `totalUnreadCount` is not updating when
|
||||
app is resumed from background mode
|
||||
app is resumed from background mode.
|
||||
- Fix retry mechanism failing in some cases.
|
||||
|
||||
## 3.3.0
|
||||
|
||||
|
||||
@@ -1538,6 +1538,7 @@ class ChannelClientState {
|
||||
members: List.from(
|
||||
channelState.members..removeWhere((m) => m.userId == user!.id),
|
||||
),
|
||||
read: channelState.read..removeWhere((r) => r.user.id == user!.id),
|
||||
));
|
||||
}));
|
||||
}
|
||||
@@ -1698,7 +1699,7 @@ class ChannelClientState {
|
||||
}
|
||||
|
||||
_channelState = _channelState.copyWith(
|
||||
messages: newMessages,
|
||||
messages: newMessages..sort(_sortByCreatedAt),
|
||||
channel: _channelState.channel?.copyWith(
|
||||
lastMessageAt: message.createdAt,
|
||||
),
|
||||
|
||||
@@ -1316,6 +1316,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.
|
||||
|
||||
@@ -71,14 +71,15 @@ class RetryQueue {
|
||||
/// Add a list of messages
|
||||
void add(List<Message> messages) {
|
||||
if (messages.isEmpty) return;
|
||||
if (_messageQueue.containsAllMessage(messages)) return;
|
||||
if (!_messageQueue.containsAllMessage(messages)) {
|
||||
logger?.info('Adding ${messages.length} messages');
|
||||
final messageList = _messageQueue.toList();
|
||||
// we should not add message if already available in the queue
|
||||
_messageQueue.addAll(messages.where(
|
||||
(it) => !messageList.any((m) => m.id == it.id),
|
||||
));
|
||||
}
|
||||
|
||||
logger?.info('Adding ${messages.length} messages');
|
||||
final messageList = _messageQueue.toList();
|
||||
// we should not add message if already available in the queue
|
||||
_messageQueue.addAll(messages.where(
|
||||
(it) => !messageList.any((m) => m.id == it.id),
|
||||
));
|
||||
_startRetrying();
|
||||
}
|
||||
|
||||
@@ -90,17 +91,21 @@ class RetryQueue {
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
logger?.info('${_messageQueue.length} messages remaining in the queue');
|
||||
final message = _messageQueue.first;
|
||||
await _runAndRetry(message);
|
||||
final succeeded = await _runAndRetry(message);
|
||||
if (!succeeded) {
|
||||
_messageQueue.toList().forEach(_sendFailedEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_isRetrying = false;
|
||||
}
|
||||
|
||||
Future<void> _runAndRetry(Message message) async {
|
||||
Future<bool> _runAndRetry(Message message) async {
|
||||
var attempt = 1;
|
||||
|
||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||
// early return in case maxAttempt is less than 0
|
||||
if (attempt > maxAttempt) return;
|
||||
if (attempt > maxAttempt) return false;
|
||||
|
||||
// ignore: literal_only_boolean_expressions
|
||||
while (true) {
|
||||
@@ -109,8 +114,12 @@ class RetryQueue {
|
||||
await _retryMessage(message);
|
||||
logger?.info('Message (${message.id}) sent successfully');
|
||||
_messageQueue.removeMessage(message);
|
||||
break;
|
||||
} on StreamChatError catch (e) {
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e is! StreamChatNetworkError || !e.isRetriable) {
|
||||
_messageQueue.removeMessage(message);
|
||||
return true;
|
||||
}
|
||||
// retry logic
|
||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||
if (attempt < maxAttempt) {
|
||||
@@ -143,16 +152,9 @@ class RetryQueue {
|
||||
_sendFailedEvent(message);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.info(
|
||||
'API call failed due to unknown error (attempt $attempt). '
|
||||
'Giving up for now, will retry when connection recovers. '
|
||||
'Error was $e',
|
||||
);
|
||||
_sendFailedEvent(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _sendFailedEvent(Message message) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,3 +442,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);
|
||||
}
|
||||
|
||||
@@ -273,3 +273,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?;
|
||||
|
||||
@@ -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.0';
|
||||
const PACKAGE_VERSION = '3.3.1';
|
||||
|
||||
@@ -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.0
|
||||
version: 3.3.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
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/stream_chat.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../fakes.dart';
|
||||
@@ -2314,6 +2301,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''',
|
||||
() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user