diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml
index 8ced7815..240c0310 100644
--- a/.github/workflows/stream_flutter_workflow.yml
+++ b/.github/workflows/stream_flutter_workflow.yml
@@ -1,4 +1,3 @@
-
name: stream_flutter_workflow
env:
@@ -70,7 +69,23 @@ jobs:
find . -maxdepth 12 -name "*.java" -print0 \| xargs -0 java -jar $HOME/google-java-format.jar --replace
./.github/workflows/scripts/validate-formatting.sh
- test:
+ test_dart:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ fetch-depth: 0
+ - name: 'Install Flutter'
+ run: ./.github/workflows/scripts/install-flutter.sh stable
+ - name: 'Install Tools'
+ run: ./.github/workflows/scripts/install-tools.sh
+ - name: 'Bootstrap Workspace'
+ run: melos bootstrap
+ - name: 'Flutter Test'
+ run: cd packages/dart_client && flutter pub run test
+
+ test_flutter:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
@@ -85,5 +100,5 @@ jobs:
run: melos bootstrap
- name: 'Flutter Test'
run: |
- melos exec -c 3 --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
+ melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test
\ No newline at end of file
diff --git a/melos.yaml b/melos.yaml
index 7e02909f..6a6c74dc 100644
--- a/melos.yaml
+++ b/melos.yaml
@@ -33,8 +33,12 @@ scripts:
flutter build macos
- test: >
- melos exec -c 1 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
+ test:dart: >
+ melos exec -c 1 --fail-fast --no-flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
+ flutter pub run test
+
+ test:flutter: >
+ melos exec -c 1 --fail-fast --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test
test:web: >
diff --git a/packages/dart_client/CHANGELOG.md b/packages/dart_client/CHANGELOG.md
index fdaee098..98730359 100644
--- a/packages/dart_client/CHANGELOG.md
+++ b/packages/dart_client/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 1.0.0-beta
+
+- 🛑 **BREAKING** Renamed `Client` to less generic `StreamChatClient`
+- 🛑 **BREAKING** Segregated the persistence layer into separate package (stream_chat_persistence)[https://pub.dev/packages/stream_chat_persistence]
+- 🛑 **BREAKING** Moved `Client.backgroundKeepAlive` to (core package)[https://pub.dev/packages/stream_chat_core]
+- 🛑 **BREAKING** Moved `Client.showLocalNotification` to (core package)[https://pub.dev/packages/stream_chat_core] and renamed it to `StreamChatCore.onBackgroundEventReceived`
+- Removed `flutter` dependency. This is now a pure Dart package 🥳
+- Minor improvements and bugfixes
+
## 0.2.24+2
- Fix reconnection bug while using tokenProvider
diff --git a/packages/dart_client/README.md b/packages/dart_client/README.md
index 241c2f4e..e0ed18bf 100644
--- a/packages/dart_client/README.md
+++ b/packages/dart_client/README.md
@@ -14,7 +14,7 @@ You can sign up for a Stream account at https://getstream.io/chat/
```yaml
dependencies:
- stream_chat: ^0.2.0
+ stream_chat: ^1.0.0-beta
```
You should then run `flutter packages get`
@@ -28,7 +28,7 @@ There is a detailed Flutter example project in the `example` folder. You can dir
First you need to instantiate a chat client. The Chat client will manage API call, event handling and manage the websocket connection to Stream Chat servers. You should only create the client once and re-use it across your application.
```dart
-final client = Client("stream-chat-api-key");
+final client = StreamChatClient("stream-chat-api-key");
```
### Logging
@@ -40,7 +40,7 @@ By default the Chat Client will write all messages with level Warn or Error to s
During development you might want to enable more logging information, you can change the default log level when constructing the client.
```dart
-final client = Client("stream-chat-api-key", logLevel: Level.INFO);
+final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
```
#### Custom Logger
@@ -52,32 +52,39 @@ myLogHandlerFunction = (LogRecord record) {
// do something with the record (ie. send it to Sentry or Fabric)
}
-final client = Client("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
+final client = StreamChatClient("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
```
### Offline storage
-By default the library saves information about channels and messages in a SQLite DB.
+To add data persistance you can extend the class `ChatPersistenceClient` and pass an instance to the `StreamChatClient`.
-Set the property `persistenceEnabled` to false if you don't want to use the offline storage.
+```dart
+class CustomChatPersistentClient extends ChatPersistenceClient {
+...
+}
-## Flutter Web
-
-Due to Moor web (for offline storage) you need to include the sql.js library:
-
-```html
-
-
-
-
-
-
-
-
-
+final client = StreamChatClient(
+ apiKey ?? kDefaultStreamApiKey,
+ logLevel: Level.INFO,
+)..chatPersistenceClient = CustomChatPersistentClient();
```
-You can grab the latest version of sql-wasm.js and sql-wasm.wasm [here](https://github.com/sql-js/sql.js/releases) and copy them into your `/web` folder.
+We provide an official persistent client in the (stream_chat_persistence)[https://pub.dev/packages/stream_chat_persistence] package.
+
+```dart
+import 'package:stream_chat_persistence/stream_chat_persistence.dart';
+
+final chatPersistentClient = StreamChatPersistenceClient(
+ logLevel: Level.INFO,
+ connectionMode: ConnectionMode.background,
+);
+
+final client = StreamChatClient(
+ apiKey ?? kDefaultStreamApiKey,
+ logLevel: Level.INFO,
+)..chatPersistenceClient = chatPersistentClient;
+```
## Contributing
diff --git a/packages/dart_client/build.yaml b/packages/dart_client/build.yaml
index ea58f519..ddbd70dd 100644
--- a/packages/dart_client/build.yaml
+++ b/packages/dart_client/build.yaml
@@ -1,9 +1,6 @@
targets:
$default:
builders:
- moor_generator:
- options:
- generate_connect_constructor: true
json_serializable:
options:
explicit_to_json: true
diff --git a/packages/dart_client/example/lib/main.dart b/packages/dart_client/example/lib/main.dart
index ed21df76..769e6d44 100644
--- a/packages/dart_client/example/lib/main.dart
+++ b/packages/dart_client/example/lib/main.dart
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
Future main() async {
- /// Create a new instance of [Client] passing the apikey obtained from your
+ /// Create a new instance of [StreamChatClient] passing the apikey obtained from your
/// project dashboard.
- final client = Client('b67pax5b2wdq');
+ final client = StreamChatClient('b67pax5b2wdq');
/// Set the current user. In a production scenario, this should be done using
/// a backend to generate a user token using our server SDK.
@@ -48,9 +48,9 @@ class StreamExample extends StatelessWidget {
@required this.channel,
}) : super(key: key);
- /// Instance of [Client] we created earlier. This contains information about
+ /// Instance of [StreamChatClient] we created earlier. This contains information about
/// our application and connection state.
- final Client client;
+ final StreamChatClient client;
/// The channel we'd like to observe and participate.
final Channel channel;
@@ -242,7 +242,7 @@ class _MessageViewState extends State {
}
}
-/// Helper extension for quickly retrieving the current user id from a [Client].
-extension on Client {
+/// Helper extension for quickly retrieving the current user id from a [StreamChatClient].
+extension on StreamChatClient {
String get uid => state.user.id;
}
diff --git a/packages/dart_client/lib/src/api/channel.dart b/packages/dart_client/lib/src/api/channel.dart
index d8ef1a03..449a5267 100644
--- a/packages/dart_client/lib/src/api/channel.dart
+++ b/packages/dart_client/lib/src/api/channel.dart
@@ -160,8 +160,8 @@ class Channel {
state?.channelStateStream?.map((cs) => cs.channel?.extraData);
/// The main Stream chat client
- Client get client => _client;
- final Client _client;
+ StreamChatClient get client => _client;
+ final StreamChatClient _client;
String get _channelURL => '/channels/$type/$id';
@@ -183,7 +183,7 @@ class Channel {
user: _client.state.user,
id: messageId,
quotedMessage: quotedMessage,
- status: MessageSendingStatus.SENDING,
+ status: MessageSendingStatus.sending,
);
if (message.parentId != null && message.id == null) {
@@ -419,7 +419,7 @@ class Channel {
}
}
- await _client.offlineStorage?.deleteMessages([messageId]);
+ await _client.chatPersistenceClient?.deleteMessageById(messageId);
}
return res;
@@ -487,9 +487,9 @@ class Channel {
PaginationParams options, {
bool preferOffline = false,
}) async {
- final cachedReplies = await _client.offlineStorage?.getReplies(
+ final cachedReplies = await _client.chatPersistenceClient?.getReplies(
parentId,
- lessThan: options?.lessThan,
+ options: options,
);
if (cachedReplies != null && cachedReplies.isNotEmpty) {
state?.updateThreadInfo(parentId, cachedReplies);
@@ -603,11 +603,10 @@ class Channel {
}
if (preferOffline && cid != null) {
- final updatedState = await _client.offlineStorage?.getChannel(
+ final updatedState =
+ await _client.chatPersistenceClient?.getChannelStateByCid(
cid,
- limit: messagesPagination?.limit,
- messageLessThan: messagesPagination?.lessThan,
- messageGreaterThan: messagesPagination?.greaterThan,
+ messagePagination: messagesPagination,
);
if (updatedState != null && updatedState.messages.isNotEmpty) {
if (state == null) {
@@ -723,7 +722,7 @@ class Channel {
});
}
- /// Hides the channel from [Client.queryChannels] for the user until a message is added
+ /// Hides the channel from [StreamChatClient.queryChannels] for the user until a message is added
/// If [clearHistory] is set to true - all messages will be removed for the user
Future hide({bool clearHistory = false}) async {
_checkInitialized();
@@ -732,7 +731,7 @@ class Channel {
if (clearHistory == true) {
state.truncate();
- await _client.offlineStorage?.deleteChannelsMessages([_cid]);
+ await _client.chatPersistenceClient?.deleteMessageByCid(_cid);
}
return _client.decode(response.data, EmptyResponse.fromJson);
@@ -871,7 +870,7 @@ class ChannelClientState {
_computeInitialUnread();
- _channel._client.offlineStorage
+ _channel._client.chatPersistenceClient
?.getChannelThreads(_channel.cid)
?.then((threads) {
_threads = threads;
@@ -949,8 +948,8 @@ class ChannelClientState {
.on(EventType.channelTruncated, EventType.notificationChannelTruncated)
.listen((event) async {
final channel = event.channel;
- await _channel._client.offlineStorage
- ?.deleteChannelsMessages([channel.cid]);
+ await _channel._client.chatPersistenceClient
+ ?.deleteMessageByCid(channel.cid);
truncate();
}));
}
@@ -978,7 +977,7 @@ class ChannelClientState {
[...messages, ...threads.values.expand((v) => v)]
.where((message) =>
message.status != null &&
- message.status != MessageSendingStatus.SENT &&
+ message.status != MessageSendingStatus.sent &&
message.createdAt.isBefore(DateTime.now().subtract(Duration(
seconds: 1,
))))
@@ -1404,7 +1403,7 @@ class ChannelClientState {
set _channelState(ChannelState v) {
_channelStateController.add(v);
- _channel._client.offlineStorage?.updateChannelState(v);
+ _channel._client.chatPersistenceClient?.updateChannelState(v);
}
/// The channel threads related to this channel
@@ -1417,9 +1416,9 @@ class ChannelClientState {
BehaviorSubject.seeded({});
set _threads(Map> v) {
- _channel._client.offlineStorage?.updateMessages(
- v.values.expand((v) => v).toList(),
+ _channel._client.chatPersistenceClient?.updateMessages(
_channel.cid,
+ v.values.expand((v) => v).toList(),
);
_threadsController.add(v);
}
diff --git a/packages/dart_client/lib/src/api/responses.dart b/packages/dart_client/lib/src/api/responses.dart
index 919a875b..e27ac2c9 100644
--- a/packages/dart_client/lib/src/api/responses.dart
+++ b/packages/dart_client/lib/src/api/responses.dart
@@ -17,7 +17,7 @@ class _BaseResponse {
String duration;
}
-/// Model response for [Client.resync] api call
+/// Model response for [StreamChatClient.resync] api call
@JsonSerializable(createToJson: false)
class SyncResponse extends _BaseResponse {
/// The list of events
@@ -28,7 +28,7 @@ class SyncResponse extends _BaseResponse {
_$SyncResponseFromJson(json);
}
-/// Model response for [Client.queryChannels] api call
+/// Model response for [StreamChatClient.queryChannels] api call
@JsonSerializable(createToJson: false)
class QueryChannelsResponse extends _BaseResponse {
/// List of channels state returned by the query
@@ -39,7 +39,7 @@ class QueryChannelsResponse extends _BaseResponse {
_$QueryChannelsResponseFromJson(json);
}
-/// Model response for [Client.queryChannels] api call
+/// Model response for [StreamChatClient.queryChannels] api call
@JsonSerializable(createToJson: false)
class TranslateMessageResponse extends _BaseResponse {
/// List of channels state returned by the query
@@ -50,7 +50,7 @@ class TranslateMessageResponse extends _BaseResponse {
_$TranslateMessageResponseFromJson(json);
}
-/// Model response for [Client.queryChannels] api call
+/// Model response for [StreamChatClient.queryChannels] api call
@JsonSerializable(createToJson: false)
class QueryMembersResponse extends _BaseResponse {
/// List of channels state returned by the query
@@ -61,7 +61,7 @@ class QueryMembersResponse extends _BaseResponse {
_$QueryMembersResponseFromJson(json);
}
-/// Model response for [Client.queryUsers] api call
+/// Model response for [StreamChatClient.queryUsers] api call
@JsonSerializable(createToJson: false)
class QueryUsersResponse extends _BaseResponse {
/// List of users returned by the query
@@ -94,7 +94,7 @@ class QueryRepliesResponse extends _BaseResponse {
_$QueryRepliesResponseFromJson(json);
}
-/// Model response for [Client.getDevices] api call
+/// Model response for [StreamChatClient.getDevices] api call
@JsonSerializable(createToJson: false)
class ListDevicesResponse extends _BaseResponse {
/// List of user devices
@@ -141,7 +141,7 @@ class SendReactionResponse extends _BaseResponse {
_$SendReactionResponseFromJson(json);
}
-/// Model response for [Client.setGuestUser] api call
+/// Model response for [StreamChatClient.setGuestUser] api call
@JsonSerializable(createToJson: false)
class SetGuestUserResponse extends _BaseResponse {
/// Guest user access token
@@ -155,7 +155,7 @@ class SetGuestUserResponse extends _BaseResponse {
_$SetGuestUserResponseFromJson(json);
}
-/// Model response for [Client.updateUser] api call
+/// Model response for [StreamChatClient.updateUser] api call
@JsonSerializable(createToJson: false)
class UpdateUsersResponse extends _BaseResponse {
/// Updated users
@@ -166,7 +166,7 @@ class UpdateUsersResponse extends _BaseResponse {
_$UpdateUsersResponseFromJson(json);
}
-/// Model response for [Client.updateMessage] api call
+/// Model response for [StreamChatClient.updateMessage] api call
@JsonSerializable(createToJson: false)
class UpdateMessageResponse extends _BaseResponse {
/// Message returned by the api call
@@ -188,7 +188,7 @@ class SendMessageResponse extends _BaseResponse {
_$SendMessageResponseFromJson(json);
}
-/// Model response for [Client.getMessage] api call
+/// Model response for [StreamChatClient.getMessage] api call
@JsonSerializable(createToJson: false)
class GetMessageResponse extends _BaseResponse {
/// Message returned by the api call
@@ -208,7 +208,7 @@ class GetMessageResponse extends _BaseResponse {
}
}
-/// Model response for [Client.search] api call
+/// Model response for [StreamChatClient.search] api call
@JsonSerializable(createToJson: false)
class SearchMessagesResponse extends _BaseResponse {
/// List of messages returned by the api call
diff --git a/packages/dart_client/lib/src/api/retry_policy.dart b/packages/dart_client/lib/src/api/retry_policy.dart
index 43af3b8e..2c59ec48 100644
--- a/packages/dart_client/lib/src/api/retry_policy.dart
+++ b/packages/dart_client/lib/src/api/retry_policy.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/foundation.dart';
+import 'package:meta/meta.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart';
@@ -15,17 +15,18 @@ class RetryPolicy {
int attempt = 0;
/// This function evaluates if we should retry the failure
- final bool Function(Client client, int attempt, ApiError apiError)
+ final bool Function(StreamChatClient client, int attempt, ApiError apiError)
shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
- final Duration Function(Client client, int attempt, ApiError apiError)
- retryTimeout;
+ final Duration Function(
+ StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
RetryPolicy copyWith({
- bool Function(Client client, int attempt, ApiError apiError) shouldRetry,
- Duration Function(Client client, int attempt, ApiError apiError)
+ bool Function(StreamChatClient client, int attempt, ApiError apiError)
+ shouldRetry,
+ Duration Function(StreamChatClient client, int attempt, ApiError apiError)
retryTimeout,
int attempt,
}) =>
diff --git a/packages/dart_client/lib/src/api/retry_queue.dart b/packages/dart_client/lib/src/api/retry_queue.dart
index 12e8377a..534d9c08 100644
--- a/packages/dart_client/lib/src/api/retry_queue.dart
+++ b/packages/dart_client/lib/src/api/retry_queue.dart
@@ -1,8 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
+import 'package:meta/meta.dart';
import 'package:logging/logging.dart';
import 'package:stream_chat/src/api/channel.dart';
import 'package:stream_chat/src/api/retry_policy.dart';
@@ -113,30 +112,30 @@ class RetryQueue {
}
void _sendFailedEvent(Message message) {
- final newStatus = message.status == MessageSendingStatus.SENDING
- ? MessageSendingStatus.FAILED
- : (message.status == MessageSendingStatus.UPDATING
- ? MessageSendingStatus.FAILED_UPDATE
- : MessageSendingStatus.FAILED_DELETE);
+ final newStatus = message.status == MessageSendingStatus.sending
+ ? MessageSendingStatus.failed
+ : (message.status == MessageSendingStatus.updating
+ ? MessageSendingStatus.failed_update
+ : MessageSendingStatus.failed_delete);
channel.state.addMessage(message.copyWith(
status: newStatus,
));
}
Future _sendMessage(Message message) async {
- if (message.status == MessageSendingStatus.FAILED_UPDATE ||
- message.status == MessageSendingStatus.UPDATING) {
+ if (message.status == MessageSendingStatus.failed_update ||
+ message.status == MessageSendingStatus.updating) {
await channel.client.updateMessage(
message,
channel.cid,
);
- } else if (message.status == MessageSendingStatus.FAILED ||
- message.status == MessageSendingStatus.SENDING) {
+ } else if (message.status == MessageSendingStatus.failed ||
+ message.status == MessageSendingStatus.sending) {
await channel.sendMessage(
message,
);
- } else if (message.status == MessageSendingStatus.FAILED_DELETE ||
- message.status == MessageSendingStatus.DELETING) {
+ } else if (message.status == MessageSendingStatus.failed_delete ||
+ message.status == MessageSendingStatus.deleting) {
await channel.client.deleteMessage(
message,
channel.cid,
@@ -152,15 +151,15 @@ class RetryQueue {
messageList.indexWhere((m) => m.id == event.message.id);
if (messageIndex == -1 &&
[
- MessageSendingStatus.FAILED_UPDATE,
- MessageSendingStatus.FAILED,
- MessageSendingStatus.FAILED_DELETE,
+ MessageSendingStatus.failed_update,
+ MessageSendingStatus.failed,
+ MessageSendingStatus.failed_delete,
].contains(event.message.status)) {
logger?.info('add message from events');
add([event.message]);
} else if (messageIndex != -1 &&
[
- MessageSendingStatus.SENT,
+ MessageSendingStatus.sent,
null,
].contains(event.message.status)) {
_messageQueue.remove(messageList[messageIndex]);
@@ -184,16 +183,16 @@ class RetryQueue {
static DateTime _getMessageDate(Message m1) {
switch (m1.status) {
- case MessageSendingStatus.FAILED_DELETE:
- case MessageSendingStatus.DELETING:
+ case MessageSendingStatus.failed_delete:
+ case MessageSendingStatus.deleting:
return m1.deletedAt;
- case MessageSendingStatus.FAILED:
- case MessageSendingStatus.SENDING:
+ case MessageSendingStatus.failed:
+ case MessageSendingStatus.sending:
return m1.createdAt;
- case MessageSendingStatus.FAILED_UPDATE:
- case MessageSendingStatus.UPDATING:
+ case MessageSendingStatus.failed_update:
+ case MessageSendingStatus.updating:
return m1.updatedAt;
default:
return null;
diff --git a/packages/dart_client/lib/src/api/websocket.dart b/packages/dart_client/lib/src/api/websocket.dart
index bdd14a1e..15102c6e 100644
--- a/packages/dart_client/lib/src/api/websocket.dart
+++ b/packages/dart_client/lib/src/api/websocket.dart
@@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
+import 'package:meta/meta.dart';
import 'package:logging/logging.dart';
+import 'package:rxdart/rxdart.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../models/event.dart';
@@ -99,9 +99,18 @@ class WebSocket {
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
final int reconnectionMonitorTimeout;
+ final _connectionStatusController =
+ BehaviorSubject.seeded(ConnectionStatus.disconnected);
+
+ set _connectionStatus(ConnectionStatus status) =>
+ _connectionStatusController.add(status);
+
+ /// The current connection status value
+ ConnectionStatus get connectionStatus => _connectionStatusController.value;
+
/// This notifies of connection status changes
- final ValueNotifier connectionStatus =
- ValueNotifier(ConnectionStatus.disconnected);
+ Stream get connectionStatusStream =>
+ _connectionStatusController.stream;
String _path;
int _retryAttempt = 1;
@@ -128,7 +137,7 @@ class WebSocket {
}
_connecting = true;
- connectionStatus.value = ConnectionStatus.connecting;
+ _connectionStatus = ConnectionStatus.connecting;
logger.info('connecting to $_path');
@@ -175,7 +184,7 @@ class WebSocket {
_reconnecting = false;
_lastEventAt = DateTime.now();
- connectionStatus.value = ConnectionStatus.connected;
+ _connectionStatus = ConnectionStatus.connected;
_retryAttempt = 1;
if (!_connectionCompleter.isCompleted) {
@@ -199,7 +208,7 @@ class WebSocket {
_connecting = false;
if (!_reconnecting) {
- connectionStatus.value = ConnectionStatus.disconnected;
+ _connectionStatus = ConnectionStatus.disconnected;
}
if (!_connectionCompleter.isCompleted) {
@@ -258,7 +267,7 @@ class WebSocket {
logger.info('reconnect');
if (!_reconnecting) {
_reconnecting = true;
- connectionStatus.value = ConnectionStatus.connecting;
+ _connectionStatus = ConnectionStatus.connecting;
}
_reconnectTimer();
@@ -300,8 +309,8 @@ class WebSocket {
_cancelTimers();
_reconnecting = false;
_manuallyDisconnected = true;
- connectionStatus.value = ConnectionStatus.disconnected;
- connectionStatus.dispose();
+ _connectionStatus = ConnectionStatus.disconnected;
+ await _connectionStatusController.close();
return _channel.sink.close();
}
}
diff --git a/packages/dart_client/lib/src/client.dart b/packages/dart_client/lib/src/client.dart
index 5f534864..7f9f560a 100644
--- a/packages/dart_client/lib/src/client.dart
+++ b/packages/dart_client/lib/src/client.dart
@@ -3,24 +3,22 @@ import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
-import 'package:flutter/cupertino.dart';
import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+import 'package:pedantic/pedantic.dart' show unawaited;
import 'package:rxdart/rxdart.dart';
-import 'package:shared_preferences/shared_preferences.dart';
import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/event_type.dart';
-import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/own_user.dart';
import 'package:stream_chat/version.dart';
import 'package:uuid/uuid.dart';
-import 'package:pedantic/pedantic.dart' show unawaited;
import 'api/channel.dart';
import 'api/connection_status.dart';
import 'api/requests.dart';
import 'api/responses.dart';
import 'api/websocket.dart';
-import 'db/offline_storage.dart';
+import 'db/chat_persistence_client.dart';
import 'exceptions.dart';
import 'models/event.dart';
import 'models/message.dart';
@@ -37,15 +35,6 @@ typedef DecoderFunction = T Function(Map);
/// own backend server. Function requires a single [userId].
typedef TokenProvider = Future Function(String userId);
-/// The key used to save the userId to sharedPreferences
-const String KEY_USER_ID = 'KEY_USER_ID';
-
-/// The key used to save the token to sharedPreferences
-const String KEY_TOKEN = 'KEY_TOKEN';
-
-/// The key used to save the apiKey to sharedPreferences
-const String KEY_API_KEY = 'KEY_API_KEY';
-
/// Provider used to send push notifications.
enum PushProvider {
/// Send notifications using Google's Firebase Cloud Messaging
@@ -75,31 +64,27 @@ extension on PushProvider {
/// websocket connection to Stream Chat servers.
///
/// ```dart
-/// final client = Client("stream-chat-api-key");
+/// final client = StreamChatClient("stream-chat-api-key");
/// ```
-class Client {
+class StreamChatClient {
/// Create a client instance with default options.
/// You should only create the client once and re-use it across your application.
- Client(
+ StreamChatClient(
this.apiKey, {
this.tokenProvider,
this.baseURL = _defaultBaseURL,
this.logLevel = Level.WARNING,
this.logHandlerFunction,
- this.persistenceEnabled = true,
Duration connectTimeout = const Duration(seconds: 6),
Duration receiveTimeout = const Duration(seconds: 6),
Dio httpClient,
- this.showLocalNotification,
- this.backgroundKeepAlive = const Duration(minutes: 1),
RetryPolicy retryPolicy,
}) {
- WidgetsFlutterBinding.ensureInitialized();
-
_retryPolicy ??= RetryPolicy(
- retryTimeout: (Client client, int attempt, ApiError error) =>
+ retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
Duration(seconds: 1 * attempt),
- shouldRetry: (Client client, int attempt, ApiError error) => attempt < 5,
+ shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
+ attempt < 5,
);
state = ClientState(this);
@@ -110,10 +95,11 @@ class Client {
logger.info('instantiating new client');
}
- OfflineStorage _offlineStorage;
+ /// Chat persistence client
+ ChatPersistenceClient chatPersistenceClient;
- /// If true chat data will persist on disk
- final bool persistenceEnabled;
+ /// Whether the chat persistence is available or not
+ bool get persistenceEnabled => chatPersistenceClient != null;
RetryPolicy _retryPolicy;
@@ -122,25 +108,14 @@ class Client {
/// The retry policy options getter
RetryPolicy get retryPolicy => _retryPolicy;
- /// Method used to show a local notification while the app is in background
- /// Switching to another application will not disconnect the client immediately
- /// So, use this method to show the notification when receiving a new message via events
- final void Function(Message, ChannelModel) showLocalNotification;
-
- /// The amount of time that will pass before disconnecting the client in the background
- final Duration backgroundKeepAlive;
-
- /// Client offline database
- OfflineStorage get offlineStorage => _offlineStorage;
-
/// This client state
ClientState state;
- /// By default the Chat Client will write all messages with level Warn or Error to stdout.
+ /// By default the Chat client will write all messages with level Warn or Error to stdout.
/// During development you might want to enable more logging information, you can change the default log level when constructing the client.
///
/// ```dart
- /// final client = Client("stream-chat-api-key", logLevel: Level.INFO);
+ /// final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
/// ```
final Level logLevel;
@@ -159,7 +134,7 @@ class Client {
/// // do something with the record (ie. send it to Sentry or Fabric)
/// }
///
- /// final client = Client("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
+ /// final client = StreamChatClient("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
///```
LogHandlerFunction logHandlerFunction;
@@ -183,7 +158,8 @@ class Client {
static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com';
static const _tokenExpiredErrorCode = 40;
- VoidCallback _connectionStatusListener;
+ StreamSubscription _connectionStatusSubscription;
+ Future Function(ConnectionStatus) _connectionStatusHandler;
final BehaviorSubject _controller = BehaviorSubject();
@@ -191,10 +167,20 @@ class Client {
/// Listen to this or use the [on] method to filter specific event types
Stream get stream => _controller.stream;
+ final _wsConnectionStatusController =
+ BehaviorSubject.seeded(ConnectionStatus.disconnected);
+
+ set _wsConnectionStatus(ConnectionStatus status) =>
+ _wsConnectionStatusController.add(status);
+
+ /// The current status value of the websocket connection
+ ConnectionStatus get wsConnectionStatus =>
+ _wsConnectionStatusController.value;
+
/// This notifies the connection status of the websocket connection.
/// Listen to this to get notified when the websocket tries to reconnect.
- final ValueNotifier wsConnectionStatus =
- ValueNotifier(ConnectionStatus.disconnected);
+ Stream get wsConnectionStatusStream =>
+ _wsConnectionStatusController.stream;
/// The current user token
String token;
@@ -342,12 +328,12 @@ class Client {
/// Call this function to dispose the client
void dispose() async {
- await _offlineStorage?.disconnect();
+ await chatPersistenceClient?.disconnect();
await _disconnect();
httpClient.close();
await _controller.close();
- state.channels.values.forEach((c) => c.dispose());
state.dispose();
+ await _wsConnectionStatusController.close();
}
Map get _httpHeaders => {
@@ -372,11 +358,6 @@ class Client {
this.token = token;
_anonymous = false;
- final sharedPreferences = await SharedPreferences.getInstance();
- await sharedPreferences.setString(KEY_USER_ID, user.id);
- await sharedPreferences.setString(KEY_TOKEN, token);
- await sharedPreferences.setString(KEY_API_KEY, apiKey);
-
return connect().then((event) {
_connectCompleter.complete(event);
return event;
@@ -424,8 +405,8 @@ class Client {
if (!event.isLocal) {
if (_synced && event.createdAt != null) {
- await _offlineStorage?.updateConnectionInfo(event);
- await _offlineStorage?.updateLastSyncAt(event.createdAt);
+ await chatPersistenceClient?.updateConnectionInfo(event);
+ await chatPersistenceClient?.updateLastSyncAt(event.createdAt);
}
}
@@ -444,21 +425,20 @@ class Client {
/// Connect the client websocket
Future connect() async {
logger.info('connecting');
- if (wsConnectionStatus.value == ConnectionStatus.connecting) {
+ if (wsConnectionStatus == ConnectionStatus.connecting) {
logger.warning('Already connecting');
throw Exception('Already connecting');
}
- if (wsConnectionStatus.value == ConnectionStatus.connected) {
+ if (wsConnectionStatus == ConnectionStatus.connected) {
logger.warning('Already connected');
throw Exception('Already connected');
}
- wsConnectionStatus.value = ConnectionStatus.connecting;
+ _wsConnectionStatus = ConnectionStatus.connecting;
- if (persistenceEnabled && _offlineStorage == null) {
- _offlineStorage =
- await connectDatabase(state.user, _detachedLogger('💽'));
+ if (persistenceEnabled) {
+ await chatPersistenceClient.connect(state.user.id);
}
_ws = WebSocket(
@@ -478,15 +458,16 @@ class Client {
logger: _detachedLogger('🔌'),
);
- _connectionStatusListener = () async {
- final value = _ws.connectionStatus.value;
- wsConnectionStatus.value = value;
- handleEvent(Event(
- type: EventType.connectionChanged,
- online: value == ConnectionStatus.connected,
- ));
+ _connectionStatusHandler = (ConnectionStatus status) async {
+ _wsConnectionStatus = status;
+ handleEvent(
+ Event(
+ type: EventType.connectionChanged,
+ online: status == ConnectionStatus.connected,
+ ),
+ );
- if (value == ConnectionStatus.connected &&
+ if (status == ConnectionStatus.connected &&
state.channels?.isNotEmpty == true) {
unawaited(queryChannels(filter: {
'cid': {
@@ -506,12 +487,13 @@ class Client {
}
};
- _ws.connectionStatus.addListener(_connectionStatusListener);
+ _connectionStatusSubscription =
+ _ws.connectionStatusStream.listen(_connectionStatusHandler);
- var event = await _offlineStorage?.getConnectionInfo();
+ var event = await chatPersistenceClient?.getConnectionInfo();
await _ws.connect().then((e) async {
- await _offlineStorage?.updateConnectionInfo(e);
+ await chatPersistenceClient?.updateConnectionInfo(e);
event = e;
await resync();
}).catchError((err, stacktrace) {
@@ -526,14 +508,14 @@ class Client {
/// Get the events missed while offline to sync the offline storage
Future resync([List cids]) async {
- final lastSyncAt = await offlineStorage?.getLastSyncAt();
+ final lastSyncAt = await chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) {
_synced = true;
return;
}
- cids ??= await offlineStorage?.getChannelCids();
+ cids ??= await chatPersistenceClient?.getChannelCids();
if (cids?.isEmpty == true) {
return;
@@ -562,7 +544,7 @@ class Client {
handleEvent(event);
});
- await _offlineStorage?.updateLastSyncAt(DateTime.now());
+ await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
_synced = true;
} catch (error) {
logger.severe('Error during resync $error');
@@ -590,7 +572,7 @@ class Client {
logger.info('awaiting connection completer');
await _connectCompleter.future;
}
- if (wsConnectionStatus.value != ConnectionStatus.connected) {
+ if (wsConnectionStatus != ConnectionStatus.connected) {
final errorMessage =
'You cannot use queryChannels without an active connection. Please call setUser to connect the client.';
if (persistenceEnabled) {
@@ -708,7 +690,7 @@ class Client {
channels.add(channel);
} else {
final newChannel = Channel.fromState(this, channelState);
- await _offlineStorage
+ await chatPersistenceClient
?.updateChannelState(newChannel.state.channelState);
newChannel.state?.updateChannelState(channelState);
newChannels[newChannel.cid] = newChannel;
@@ -719,7 +701,7 @@ class Client {
state.channels = newChannels;
- await _offlineStorage?.updateChannelQueries(
+ await chatPersistenceClient?.updateChannelQueries(
filter,
res.channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
@@ -754,7 +736,7 @@ class Client {
@required List sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
}) async {
- final offlineChannels = await _offlineStorage?.getChannelStates(
+ final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
sort: sort,
paginationParams: paginationParams,
@@ -769,7 +751,8 @@ class Client {
return channel;
} else {
final newChannel = Channel.fromState(this, channelState);
- _offlineStorage?.updateChannelState(newChannel.state.channelState);
+ chatPersistenceClient
+ ?.updateChannelState(newChannel.state.channelState);
newChannels[newChannel.cid] = newChannel;
return newChannel;
}
@@ -923,17 +906,17 @@ class Client {
}
/// Closes the websocket connection and resets the client
- /// If [flushOfflineStorage] is true the client deletes all offline user's data
+ /// If [flushChatPersistence] is true the client deletes all offline user's data
/// If [clearUser] is true the client unsets the current user
Future disconnect({
- bool flushOfflineStorage = false,
+ bool flushChatPersistence = false,
bool clearUser = false,
}) async {
logger.info(
- 'Disconnecting flushOfflineStorage: $flushOfflineStorage; clearUser: $clearUser');
+ 'Disconnecting flushOfflineStorage: $flushChatPersistence; clearUser: $clearUser');
- await _offlineStorage?.disconnect(flush: flushOfflineStorage);
- _offlineStorage = null;
+ await chatPersistenceClient?.disconnect(flush: flushChatPersistence);
+ chatPersistenceClient = null;
if (clearUser == true) {
state.dispose();
@@ -947,6 +930,7 @@ class Client {
logger.info('Client disconnecting');
await _ws?.disconnect();
+ await _connectionStatusSubscription?.cancel();
}
/// Requests users with a given query.
@@ -1196,7 +1180,7 @@ class Client {
String cid,
]) async {
message = message.copyWith(
- status: MessageSendingStatus.UPDATING,
+ status: MessageSendingStatus.updating,
updatedAt: message.updatedAt ?? DateTime.now(),
);
@@ -1227,10 +1211,10 @@ class Client {
/// Deletes the given message
Future deleteMessage(Message message, [String cid]) async {
- if (message.status == MessageSendingStatus.FAILED) {
+ if (message.status == MessageSendingStatus.failed) {
state.channels[cid].state.addMessage(message.copyWith(
type: 'deleted',
- status: MessageSendingStatus.SENT,
+ status: MessageSendingStatus.sent,
));
return EmptyResponse();
}
@@ -1238,7 +1222,7 @@ class Client {
try {
message = message.copyWith(
type: 'deleted',
- status: MessageSendingStatus.DELETING,
+ status: MessageSendingStatus.deleting,
deletedAt: message.deletedAt ?? DateTime.now(),
);
@@ -1250,7 +1234,7 @@ class Client {
if (state?.channels != null) {
state.channels[cid]?.state
- ?.addMessage(message.copyWith(status: MessageSendingStatus.SENT));
+ ?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
}
return decode(response.data, EmptyResponse.fromJson);
@@ -1322,7 +1306,7 @@ class ClientState {
void _listenChannelHidden() {
_subscriptions.add(_client.on(EventType.channelHidden).listen((event) {
- _client._offlineStorage?.deleteChannels([event.cid]);
+ _client.chatPersistenceClient?.deleteChannels([event.cid]);
if (channels != null) {
channels = channels..removeWhere((cid, ch) => cid == event.cid);
}
@@ -1347,14 +1331,14 @@ class ClientState {
)
.listen((Event event) async {
final eventChannel = event.channel;
- await _client._offlineStorage?.deleteChannels([eventChannel.cid]);
+ await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
if (channels != null) {
channels = channels..remove(eventChannel.cid);
}
}));
}
- final Client _client;
+ final StreamChatClient _client;
/// Update user information
set user(OwnUser user) {
diff --git a/packages/dart_client/lib/src/db/chat_persistence_client.dart b/packages/dart_client/lib/src/db/chat_persistence_client.dart
new file mode 100644
index 00000000..a2a4d313
--- /dev/null
+++ b/packages/dart_client/lib/src/db/chat_persistence_client.dart
@@ -0,0 +1,224 @@
+import 'package:stream_chat/src/api/requests.dart';
+import 'package:stream_chat/src/models/channel_model.dart';
+import 'package:stream_chat/src/models/channel_state.dart';
+import 'package:stream_chat/src/models/event.dart';
+import 'package:stream_chat/src/models/member.dart';
+import 'package:stream_chat/src/models/message.dart';
+import 'package:stream_chat/src/models/reaction.dart';
+import 'package:stream_chat/src/models/read.dart';
+import 'package:stream_chat/src/models/user.dart';
+
+/// A simple client used for persisting chat data locally.
+abstract class ChatPersistenceClient {
+ /// Creates a new connection to the client
+ Future connect(String userId);
+
+ /// Closes the client connection
+ /// If [flush] is true, the data will also be deleted
+ Future disconnect({bool flush = false});
+
+ /// Get stored replies by messageId
+ Future> getReplies(
+ String parentId, {
+ PaginationParams options,
+ });
+
+ /// Get stored connection event
+ Future getConnectionInfo();
+
+ /// Get stored lastSyncAt
+ Future getLastSyncAt();
+
+ /// Update stored connection event
+ Future updateConnectionInfo(Event event);
+
+ /// Update stored lastSyncAt
+ Future updateLastSyncAt(DateTime lastSyncAt);
+
+ /// Get the channel cids saved in the offline storage
+ Future> getChannelCids();
+
+ /// Get stored [ChannelModel]s by providing channel [cid]
+ Future getChannelByCid(String cid);
+
+ /// Get stored channel [Member]s by providing channel [cid]
+ Future> getMembersByCid(String cid);
+
+ /// Get stored channel [Read]s by providing channel [cid]
+ Future> getReadsByCid(String cid);
+
+ /// Get stored [Message]s by providing channel [cid]
+ ///
+ /// Optionally, you can [messagePagination]
+ /// for filtering out messages
+ Future> getMessagesByCid(
+ String cid, {
+ PaginationParams messagePagination,
+ });
+
+ /// Get [ChannelState] data by providing channel [cid]
+ Future getChannelStateByCid(
+ String cid, {
+ PaginationParams messagePagination,
+ }) async {
+ final members = await getMembersByCid(cid);
+ final reads = await getReadsByCid(cid);
+ final channel = await getChannelByCid(cid);
+ final messages = await getMessagesByCid(
+ cid,
+ messagePagination: messagePagination,
+ );
+ return ChannelState(
+ members: members,
+ read: reads,
+ messages: messages,
+ channel: channel,
+ );
+ }
+
+ /// Get all the stored [ChannelState]s
+ ///
+ /// Optionally, pass [filter], [sort], [paginationParams]
+ /// for filtering out states.
+ Future> getChannelStates({
+ Map filter,
+ List sort = const [],
+ PaginationParams paginationParams,
+ });
+
+ /// Update list of channel queries.
+ ///
+ /// If [clearQueryCache] is true before the insert
+ /// the list of matching rows will be deleted
+ Future updateChannelQueries(
+ Map filter,
+ List cids,
+ bool clearQueryCache,
+ );
+
+ /// Remove a message by [messageId]
+ Future deleteMessageById(String messageId) {
+ return deleteMessageByIds([messageId]);
+ }
+
+ /// Remove a message by [messageIds]
+ Future deleteMessageByIds(List messageIds);
+
+ /// Remove a message by channel [cid]
+ Future deleteMessageByCid(String cid) {
+ return deleteMessageByCids([cid]);
+ }
+
+ /// Remove a message by message [cids]
+ Future deleteMessageByCids(List cids);
+
+ /// Remove a channel by [cid]
+ Future deleteChannels(List cids);
+
+ /// Updates the message data of a particular channel [cid] with
+ /// the new [messages] data
+ Future updateMessages(String cid, List messages);
+
+ /// Returns all the threads by parent message of a particular channel by
+ /// providing channel [cid]
+ Future