Merge pull request #240 from GetStream/llc-independence

LLC Independence
This commit is contained in:
Salvatore Giordano
2021-02-01 15:43:07 +01:00
committed by GitHub
143 changed files with 5176 additions and 4258 deletions
+18 -3
View File
@@ -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
+6 -2
View File
@@ -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: >
+9
View File
@@ -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
+28 -21
View File
@@ -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
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script defer src="sql-wasm.js"></script>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body></body>
</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
-3
View File
@@ -1,9 +1,6 @@
targets:
$default:
builders:
moor_generator:
options:
generate_connect_constructor: true
json_serializable:
options:
explicit_to_json: true
+6 -6
View File
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
Future<void> 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<MessageView> {
}
}
/// 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;
}
+18 -19
View File
@@ -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<EmptyResponse> 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 {
<Message>[...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<String, List<Message>> 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);
}
+11 -11
View File
@@ -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
@@ -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,
}) =>
@@ -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<void> _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;
+19 -10
View File
@@ -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> connectionStatus =
ValueNotifier(ConnectionStatus.disconnected);
Stream<ConnectionStatus> 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();
}
}
+75 -91
View File
@@ -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> = T Function(Map<String, dynamic>);
/// own backend server. Function requires a single [userId].
typedef TokenProvider = Future<String> 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<ConnectionStatus> _connectionStatusSubscription;
Future<void> Function(ConnectionStatus) _connectionStatusHandler;
final BehaviorSubject<Event> _controller = BehaviorSubject<Event>();
@@ -191,10 +167,20 @@ class Client {
/// Listen to this or use the [on] method to filter specific event types
Stream<Event> 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<ConnectionStatus> wsConnectionStatus =
ValueNotifier(ConnectionStatus.disconnected);
Stream<ConnectionStatus> 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<String, String> 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<Event> 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<void> resync([List<String> 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<SortOption> 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<void> 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<EmptyResponse> 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) {
@@ -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<void> connect(String userId);
/// Closes the client connection
/// If [flush] is true, the data will also be deleted
Future<void> disconnect({bool flush = false});
/// Get stored replies by messageId
Future<List<Message>> getReplies(
String parentId, {
PaginationParams options,
});
/// Get stored connection event
Future<Event> getConnectionInfo();
/// Get stored lastSyncAt
Future<DateTime> getLastSyncAt();
/// Update stored connection event
Future<void> updateConnectionInfo(Event event);
/// Update stored lastSyncAt
Future<void> updateLastSyncAt(DateTime lastSyncAt);
/// Get the channel cids saved in the offline storage
Future<List<String>> getChannelCids();
/// Get stored [ChannelModel]s by providing channel [cid]
Future<ChannelModel> getChannelByCid(String cid);
/// Get stored channel [Member]s by providing channel [cid]
Future<List<Member>> getMembersByCid(String cid);
/// Get stored channel [Read]s by providing channel [cid]
Future<List<Read>> getReadsByCid(String cid);
/// Get stored [Message]s by providing channel [cid]
///
/// Optionally, you can [messagePagination]
/// for filtering out messages
Future<List<Message>> getMessagesByCid(
String cid, {
PaginationParams messagePagination,
});
/// Get [ChannelState] data by providing channel [cid]
Future<ChannelState> 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<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter,
List<SortOption> 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<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
bool clearQueryCache,
);
/// Remove a message by [messageId]
Future<void> deleteMessageById(String messageId) {
return deleteMessageByIds([messageId]);
}
/// Remove a message by [messageIds]
Future<void> deleteMessageByIds(List<String> messageIds);
/// Remove a message by channel [cid]
Future<void> deleteMessageByCid(String cid) {
return deleteMessageByCids([cid]);
}
/// Remove a message by message [cids]
Future<void> deleteMessageByCids(List<String> cids);
/// Remove a channel by [cid]
Future<void> deleteChannels(List<String> cids);
/// Updates the message data of a particular channel [cid] with
/// the new [messages] data
Future<void> updateMessages(String cid, List<Message> messages);
/// Returns all the threads by parent message of a particular channel by
/// providing channel [cid]
Future<Map<String, List<Message>>> getChannelThreads(String cid);
/// Updates all the channels using the new [channels] data.
Future<void> updateChannels(List<ChannelModel> channels);
/// Updates all the members of a particular channle [cid]
/// with the new [members] data
Future<void> updateMembers(String cid, List<Member> members);
/// Updates the read data of a particular channel [cid] with
/// the new [reads] data
Future<void> updateReads(String cid, List<Read> reads);
/// Updates the users data with the new [users] data
Future<void> updateUsers(List<User> users);
/// Updates the reactions data with the new [reactions] data
Future<void> updateReactions(List<Reaction> reactions);
/// Deletes all the reactions by [messageIds]
Future<void> deleteReactionsByMessageId(List<String> messageIds);
/// Deletes all the members by channel [cids]
Future<void> deleteMembersByCids(List<String> cids);
/// Update the channel state data using [channelState]
Future<void> updateChannelState(ChannelState channelState) {
return updateChannelStates([channelState]);
}
/// Update list of channel states
Future<void> updateChannelStates(List<ChannelState> channelStates) async {
final deleteReactions = deleteReactionsByMessageId(channelStates
.expand((it) => it.messages)
.map((m) => m.id)
.toList(growable: false));
final deleteMembers = deleteMembersByCids(
channelStates.map((it) => it.channel.cid).toList(growable: false),
);
await Future.wait([
deleteReactions,
deleteMembers,
]);
final channels = channelStates.map((it) {
return it.channel;
}).where((it) => it != null);
final reactions = channelStates.expand((it) => it.messages).expand((it) {
return [
...it.ownReactions.where((r) => r.userId != null),
...it.latestReactions.where((r) => r.userId != null)
];
}).where((it) => it != null);
final users = channelStates
.map((cs) => [
cs.channel?.createdBy,
...cs.messages?.map((m) {
return [
m.user,
...m.latestReactions?.map((r) => r.user),
...m.ownReactions?.map((r) => r.user),
];
})?.expand((v) => v),
...cs.read?.map((r) => r.user),
...cs.members?.map((m) => m.user),
])
.expand((it) => it)
.where((it) => it != null);
final updateMessagesFuture = channelStates.map((it) {
final cid = it.channel.cid;
final messages = it.messages.where((it) => it != null);
return updateMessages(cid, messages.toList(growable: false));
}).toList(growable: false);
final updateReadsFuture = channelStates.map((it) {
final cid = it.channel.cid;
final reads = it.read.where((it) => it != null);
return updateReads(cid, reads.toList(growable: false));
}).toList(growable: false);
final updateMembersFuture = channelStates.map((it) {
final cid = it.channel.cid;
final members = it.members.where((it) => it != null);
return updateMembers(cid, members.toList(growable: false));
}).toList(growable: false);
await Future.wait([
...updateMessagesFuture,
...updateReadsFuture,
...updateMembersFuture,
updateUsers(users.toList(growable: false)),
updateChannels(channels.toList(growable: false)),
updateReactions(reactions.toList(growable: false)),
]);
}
}
@@ -1,258 +0,0 @@
part of 'offline_storage.dart';
@DataClassName('ChannelQuery')
class _ChannelQueries extends Table {
TextColumn get queryHash => text()();
TextColumn get channelCid => text()();
@override
Set<Column> get primaryKey => {
queryHash,
channelCid,
};
}
class _Channels extends Table {
TextColumn get id => text()();
TextColumn get type => text()();
TextColumn get cid => text()();
TextColumn get config => text()();
BoolColumn get frozen => boolean().withDefault(Constant(false))();
DateTimeColumn get lastMessageAt => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime().nullable()();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
IntColumn get memberCount => integer().nullable()();
TextColumn get extraData => text().nullable().map(_ExtraDataConverter())();
TextColumn get createdBy => text().nullable()();
@override
Set<Column> get primaryKey => {cid};
}
class _ConnectionEvent extends Table {
IntColumn get id => integer()();
TextColumn get ownUser => text().nullable().map(_ExtraDataConverter())();
IntColumn get totalUnreadCount => integer().nullable()();
IntColumn get unreadChannels => integer().nullable()();
DateTimeColumn get lastEventAt => dateTime().nullable()();
DateTimeColumn get lastSyncAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class _Users extends Table {
TextColumn get id => text()();
TextColumn get role => text().nullable()();
DateTimeColumn get createdAt => dateTime().nullable()();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get lastActive => dateTime().nullable()();
BoolColumn get online => boolean().nullable()();
BoolColumn get banned => boolean().nullable()();
TextColumn get extraData => text().nullable().map(_ExtraDataConverter())();
@override
Set<Column> get primaryKey => {id};
}
class _Reads extends Table {
DateTimeColumn get lastRead => dateTime()();
TextColumn get userId => text()();
TextColumn get channelCid => text()();
IntColumn get unreadMessages => integer().nullable()();
@override
Set<Column> get primaryKey => {
userId,
channelCid,
};
}
class _Reactions extends Table {
TextColumn get messageId => text()();
TextColumn get type => text()();
DateTimeColumn get createdAt => dateTime()();
IntColumn get score => integer().nullable()();
TextColumn get userId => text()();
TextColumn get extraData => text().nullable().map(_ExtraDataConverter())();
@override
Set<Column> get primaryKey => {
messageId,
type,
userId,
};
}
class _Messages extends Table {
TextColumn get id => text()();
TextColumn get messageText => text().nullable()();
TextColumn get attachmentJson => text().nullable()();
IntColumn get status =>
integer().map(_MessageSendingStatusConverter()).nullable()();
TextColumn get type => text().nullable()();
List<User> mentionedUsers;
TextColumn get reactionCounts =>
text().nullable().map(_ExtraDataConverter<int>())();
TextColumn get reactionScores =>
text().nullable().map(_ExtraDataConverter<int>())();
TextColumn get parentId => text().nullable()();
TextColumn get quotedMessageId => text().nullable()();
IntColumn get replyCount => integer().nullable()();
BoolColumn get showInChannel => boolean().nullable()();
BoolColumn get shadowed => boolean().nullable()();
TextColumn get command => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get userId => text().nullable()();
TextColumn get channelCid => text().nullable()();
TextColumn get extraData => text().nullable().map(_ExtraDataConverter())();
@override
Set<Column> get primaryKey => {id};
}
class _Members extends Table {
TextColumn get userId => text()();
TextColumn get channelCid => text()();
TextColumn get role => text().nullable()();
DateTimeColumn get inviteAcceptedAt => dateTime().nullable()();
DateTimeColumn get inviteRejectedAt => dateTime().nullable()();
BoolColumn get invited => boolean().nullable()();
BoolColumn get banned => boolean().nullable()();
BoolColumn get shadowBanned => boolean().nullable()();
BoolColumn get isModerator => boolean().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {
userId,
channelCid,
};
}
class _ExtraDataConverter<T> extends TypeConverter<Map<String, T>, String> {
@override
Map<String, T> mapToDart(fromDb) {
if (fromDb == null) {
return null;
}
return Map<String, T>.from(jsonDecode(fromDb) ?? {});
}
@override
String mapToSql(value) {
return jsonEncode(value);
}
}
class _MessageSendingStatusConverter
extends TypeConverter<MessageSendingStatus, int> {
@override
MessageSendingStatus mapToDart(int fromDb) {
switch (fromDb) {
case 0:
return MessageSendingStatus.SENDING;
case 1:
return MessageSendingStatus.SENT;
case 2:
return MessageSendingStatus.FAILED;
case 3:
return MessageSendingStatus.UPDATING;
case 4:
return MessageSendingStatus.FAILED_UPDATE;
case 5:
return MessageSendingStatus.DELETING;
case 6:
return MessageSendingStatus.FAILED_DELETE;
default:
return null;
}
}
@override
int mapToSql(MessageSendingStatus value) {
switch (value) {
case MessageSendingStatus.SENDING:
return 0;
case MessageSendingStatus.SENT:
return 1;
case MessageSendingStatus.FAILED:
return 2;
case MessageSendingStatus.UPDATING:
return 3;
case MessageSendingStatus.FAILED_UPDATE:
return 4;
case MessageSendingStatus.DELETING:
return 5;
case MessageSendingStatus.FAILED_DELETE:
return 6;
default:
return null;
}
}
}
@@ -1,841 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart' show WidgetsFlutterBinding;
import 'package:logging/logging.dart';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import 'package:stream_chat/src/db/shared/shared_db.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/own_user.dart';
import '../api/requests.dart';
import '../models/attachment.dart';
import '../models/channel_config.dart';
import '../models/channel_model.dart';
import '../models/channel_state.dart';
import '../models/member.dart';
import '../models/message.dart';
import '../models/reaction.dart';
import '../models/read.dart';
import '../models/user.dart';
part 'models.part.dart';
part 'offline_storage.g.dart';
/// Gets a new instance of the database running on a background isolate
Future<OfflineStorage> connectDatabase(User user, Logger logger) async {
logger.info('Connecting on background isolate');
WidgetsFlutterBinding.ensureInitialized();
return SharedDB.constructOfflineStorage(
userId: user.id,
logger: logger,
);
}
LazyDatabase _openConnection(String userId) {
moorRuntimeOptions.dontWarnAboutMultipleDatabases = true;
return LazyDatabase(() async {
return await SharedDB.constructDatabase('db_$userId.sqlite');
});
}
/// Offline database used for caching channel queries and state
@UseMoor(tables: [
_ConnectionEvent,
_Channels,
_Users,
_Messages,
_Reads,
_Members,
_ChannelQueries,
_Reactions,
])
class OfflineStorage extends _$OfflineStorage {
/// Creates a new database instance
OfflineStorage.connect(
DatabaseConnection connection,
this._userId,
this._isolate,
this._logger,
) : super.connect(connection);
/// Instantiate a new OfflineStorage
OfflineStorage(
this._userId,
this._logger,
) : _isolate = null,
super(_openConnection(_userId)) {
_logger.info('Connecting on standard isolate');
}
final String _userId;
final MoorIsolate _isolate;
final Logger _logger;
// you should bump this number whenever you change or add a table definition. Migrations
// are covered later in this readme.
@override
int get schemaVersion => 8;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (openingDetails, before, after) async {
if (before != after) {
final m = createMigrator();
for (final table in allTables) {
await m.deleteTable(table.actualTableName);
await m.createTable(table);
}
}
},
);
/// Closes the database instance
/// If [flush] is true, the database data will be deleted
Future<void> disconnect({bool flush = false}) async {
_logger.info('Disconnecting');
if (flush) {
_logger.info('Flushing');
await batch((batch) {
allTables.forEach((table) {
delete(table).go();
});
});
}
await _isolate?.shutdownAll();
await close();
}
/// Get stored replies by messageId
Future<List<Message>> getReplies(
String parentId, {
String lessThan,
}) async {
final offlineList = await Future.wait(await (select(messages).join([
innerJoin(users, messages.userId.equalsExp(users.id)),
])
..where(messages.parentId.equals(parentId))
..orderBy([
OrderingTerm.asc(messages.createdAt),
]))
.map(_messageFromJoinRow)
.get());
if (lessThan != null) {
final lessThanIndex = offlineList.indexWhere((m) => m.id == lessThan);
offlineList.removeRange(lessThanIndex, offlineList.length);
}
return offlineList;
}
/// Get stored connection event
Future<Event> getConnectionInfo() async {
return select(connectionEvent).map((row) {
return Event(
me: row.ownUser != null ? OwnUser.fromJson(row.ownUser) : null,
totalUnreadCount: row.totalUnreadCount,
unreadChannels: row.unreadChannels,
);
}).getSingle();
}
/// Get stored lastSyncAt
Future<DateTime> getLastSyncAt() async {
return select(connectionEvent).getSingle().then((r) => r?.lastSyncAt);
}
/// Update stored connection event
Future<void> updateConnectionInfo(Event event) async {
final connectionInfo = await select(connectionEvent).getSingle();
return into(connectionEvent).insert(
_ConnectionEventData(
id: 1,
lastSyncAt: connectionInfo?.lastSyncAt,
lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt,
totalUnreadCount:
event.totalUnreadCount ?? connectionInfo?.totalUnreadCount,
ownUser: event.me?.toJson() ?? connectionInfo?.ownUser,
unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels,
),
mode: InsertMode.insertOrReplace,
);
}
/// Update stored lastSyncAt
Future<void> updateLastSyncAt(DateTime lastSyncAt) async {
return await (update(connectionEvent)..where((r) => r.id.equals(1))).write(
_ConnectionEventCompanion(
id: Value(1),
lastSyncAt: Value(lastSyncAt),
),
);
}
/// Get the channel cids saved in the offline storage
Future<List<String>> getChannelCids() async {
return (select(channels)
..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)])
..limit(250))
.map((c) => c.cid)
.get();
}
/// Get channel data by cid
Future<ChannelState> getChannel(
String cid, {
int limit,
String messageLessThan,
String messageGreaterThan,
}) async {
return await (select(channels)..where((c) => c.cid.equals(cid))).join([
leftOuterJoin(users, channels.createdBy.equalsExp(users.id)),
]).map((row) {
return _channelFromRow(
row.readTable(channels),
row.readTable(users),
limit: limit,
messageLessThan: messageLessThan,
messageGreaterThan: messageGreaterThan,
);
}).getSingle();
}
/// Get list of channels by filter, sort and paginationParams
Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter,
List<SortOption> sort = const [],
PaginationParams paginationParams,
}) async {
_logger.info('Get channel states');
final hash = _computeHash(filter);
final cachedChannels = await Future.wait(await (select(channelQueries)
..where((c) => c.queryHash.equals(hash)))
.get()
.then((channelQueries) {
final cids = channelQueries.map((c) => c.channelCid).toList();
final query = select(channels)..where((c) => c.cid.isIn(cids));
sort = sort
?.where((s) => ChannelModel.topLevelFields.contains(s.field))
?.toList();
if (sort != null && sort.isNotEmpty) {
query.orderBy(sort.map((s) {
final orderExpression = CustomExpression('channels.${s.field}');
return (c) => OrderingTerm(
expression: orderExpression,
mode: s.direction == 1 ? OrderingMode.asc : OrderingMode.desc,
);
}).toList());
}
if (paginationParams != null) {
query.limit(
paginationParams.limit ?? 10,
offset: paginationParams.offset,
);
}
return query.join([
leftOuterJoin(users, channels.createdBy.equalsExp(users.id)),
]).map((row) async {
final userRow = row.readTable(users);
final channelRow = row.readTable(channels);
return _channelFromRow(channelRow, userRow);
}).get();
}));
_logger.info('Got ${cachedChannels.length} channels');
if (sort?.isEmpty != false && cachedChannels?.isNotEmpty == true) {
cachedChannels
.sort((a, b) => b.channel.updatedAt.compareTo(a.channel.updatedAt));
cachedChannels.sort((a, b) {
final dateA = a.channel.lastMessageAt ?? a.channel.createdAt;
final dateB = b.channel.lastMessageAt ?? b.channel.createdAt;
return dateB.compareTo(dateA);
});
}
return cachedChannels;
}
/// Update list of channel queries
/// If [clearQueryCache] is true before the insert
/// the list of matching rows will be deleted
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
bool clearQueryCache,
) async {
final hash = _computeHash(filter);
if (clearQueryCache) {
await (delete(channelQueries)
..where(
(_ChannelQueries query) => query.queryHash.equals(hash),
))
.go();
}
return batch((batch) {
batch.insertAll(
channelQueries,
cids.map((cid) {
return ChannelQuery(
queryHash: hash,
channelCid: cid,
);
}).toList(),
mode: InsertMode.insertOrReplace,
);
});
}
/// Remove a message by message id
Future<void> deleteMessages(List<String> messageIds) {
return batch((batch) {
batch.deleteWhere<_Reactions, _Reaction>(
reactions,
(r) => r.messageId.isIn(messageIds),
);
batch.deleteWhere<_Messages, _Message>(
messages,
(m) => m.id.isIn(messageIds),
);
});
}
/// Remove a message by message id
Future<void> deleteChannelsMessages(List<String> cids) async {
final messageIds = await (select(messages)
..where((m) => m.channelCid.isIn(cids)))
.map((m) => m.id)
.get();
return batch((batch) {
batch.deleteWhere<_Reactions, _Reaction>(
reactions,
(r) => r.messageId.isIn(messageIds),
);
batch.deleteWhere<_Messages, _Message>(
messages,
(m) => m.id.isIn(messageIds),
);
});
}
/// Remove a channel by cid
Future<void> deleteChannels(List<String> cids) async {
await deleteChannelsMessages(cids);
return batch((batch) {
batch.deleteWhere<_Members, _Member>(
members,
(m) => m.channelCid.isIn(cids),
);
batch.deleteWhere<_Reads, _Read>(
reads,
(r) => r.channelCid.isIn(cids),
);
batch.deleteWhere<_Channels, _Channel>(
channels,
(c) => c.cid.isIn(cids),
);
});
}
/// Update messages data from a list
Future<void> updateMessages(
List<Message> newMessages,
String cid,
) {
return batch((batch) {
batch.insertAll(
messages,
newMessages.map(
(m) {
return _Message(
id: m.id,
attachmentJson: m.attachments != null
? jsonEncode(m.attachments.map((a) => a.toJson()).toList())
: null,
channelCid: cid,
type: m.type,
parentId: m.parentId,
quotedMessageId: m.quotedMessageId,
command: m.command,
createdAt: m.createdAt,
shadowed: m.shadowed,
showInChannel: m.showInChannel,
replyCount: m.replyCount,
reactionScores: m.reactionScores,
reactionCounts: m.reactionCounts,
status: m.status,
updatedAt: m.updatedAt,
extraData: m.extraData,
userId: m.user.id,
deletedAt: m.deletedAt,
messageText: m.text,
);
},
).toList(),
mode: InsertMode.insertOrReplace,
);
});
}
/// Update single channel state
Future<void> updateChannelState(ChannelState channelState) async {
await updateChannelStates([channelState]);
}
/// Update list of channel states
Future<void> updateChannelStates(List<ChannelState> channelStates) async {
channelStates.forEach((cs) {
updateMessages(
cs.messages,
cs.channel.cid,
);
});
await batch((batch) {
_updateReactions(batch, channelStates);
_updateUsers(batch, channelStates);
_updateReads(channelStates, batch);
_updateMembers(channelStates, batch);
_updateChannels(batch, channelStates);
});
}
/// Get the info about channel threads
Future<Map<String, List<Message>>> getChannelThreads(String cid) async {
final rowMessages = await Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)),
])
..where(messages.channelCid.equals(cid))
..where(isNotNull(messages.parentId))
..orderBy([
OrderingTerm.asc(messages.createdAt),
]))
.map(_messageFromJoinRow)
.get());
final threads = <String, List<Message>>{};
rowMessages.forEach((message) {
if (threads.containsKey(message.parentId)) {
threads[message.parentId].add(message);
} else {
threads[message.parentId] = [message];
}
});
return threads;
}
Future<ChannelState> _channelFromRow(
_Channel channelRow,
_User userRow, {
int limit,
String messageLessThan,
String messageGreaterThan,
}) async {
final rowMessages = await _getChannelMessages(
channelRow,
limit: limit,
lessThan: messageLessThan,
greaterThan: messageGreaterThan,
);
final rowReads = await _getChannelReads(channelRow);
final rowMembers = await _getChannelMembers(channelRow);
return ChannelState(
members: rowMembers,
read: rowReads,
messages: rowMessages,
channel: ChannelModel(
id: channelRow.id,
type: channelRow.type,
frozen: channelRow.frozen,
createdAt: channelRow.createdAt,
updatedAt: channelRow.updatedAt,
memberCount: channelRow.memberCount,
cid: channelRow.cid,
lastMessageAt: channelRow.lastMessageAt,
deletedAt: channelRow.deletedAt,
extraData: channelRow.extraData,
config: ChannelConfig.fromJson(jsonDecode(channelRow.config)),
createdBy: userRow != null ? _userFromUserRow(userRow) : null,
),
);
}
String _computeHash(Map<String, dynamic> filter) {
if (filter == null) {
return 'allchannels';
}
final hash = base64Encode(utf8.encode('filter: ${jsonEncode(filter)}'));
return hash;
}
Future<Message> _getMessageById(String id) async {
if (id == null || id.isEmpty) return null;
final message = await Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)),
])
..where(messages.id.equals(id)))
.map(_messageFromJoinRow)
.get());
return message.first;
}
Future<List<Message>> _getChannelMessages(
_Channel channelRow, {
int limit,
String lessThan,
String greaterThan,
}) async {
final rowMessages = await Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)),
])
..where(messages.channelCid.equals(channelRow.cid))
..where(
isNull(messages.parentId) | messages.showInChannel.equals(true))
..orderBy([
OrderingTerm.asc(messages.createdAt),
]))
.map(_messageFromJoinRow)
.get());
if (lessThan != null) {
final lessThanIndex = rowMessages.indexWhere((m) => m.id == lessThan);
if (lessThanIndex != -1) {
rowMessages.removeRange(lessThanIndex, rowMessages.length);
}
}
if (greaterThan != null) {
final greaterThanIndex =
rowMessages.indexWhere((m) => m.id == greaterThan);
if (greaterThanIndex != -1) {
rowMessages.removeRange(0, greaterThanIndex);
}
}
if (limit != null) {
return rowMessages.take(limit).toList();
}
return rowMessages;
}
Future<Message> _messageFromJoinRow(row) async {
final messageRow = row.readTable(messages);
final userRow = row.readTable(users);
final latestReactions = await _getLatestReactions(messageRow);
final ownReactions = await _getOwnReactions(messageRow);
final quotedMessage = await _getMessageById(messageRow.quotedMessageId);
return Message(
shadowed: messageRow.shadowed,
latestReactions: latestReactions,
ownReactions: ownReactions,
attachments: messageRow.attachmentJson != null
? List<Map<String, dynamic>>.from(
jsonDecode(messageRow.attachmentJson))
.map((j) => Attachment.fromJson(j))
.toList()
: null,
createdAt: messageRow.createdAt,
extraData: messageRow.extraData,
updatedAt: messageRow.updatedAt,
id: messageRow.id,
type: messageRow.type,
status: messageRow.status,
command: messageRow.command,
parentId: messageRow.parentId,
quotedMessageId: messageRow.quotedMessageId,
quotedMessage: quotedMessage,
reactionCounts: messageRow.reactionCounts,
reactionScores: messageRow.reactionScores,
replyCount: messageRow.replyCount,
showInChannel: messageRow.showInChannel,
text: messageRow.messageText,
user: _userFromUserRow(userRow),
deletedAt: messageRow.deletedAt,
);
}
Future<List<Reaction>> _getLatestReactions(_Message messageRow) async {
return await (select(reactions).join([
leftOuterJoin(users, reactions.userId.equalsExp(users.id)),
])
..where(reactions.messageId.equals(messageRow.id))
..orderBy([OrderingTerm.asc(reactions.createdAt)]))
.map((row) {
final r = row.readTable(reactions);
final u = row.readTable(users);
return _reactionFromRow(r, u);
}).get();
}
Reaction _reactionFromRow(_Reaction r, _User u) {
return Reaction(
extraData: r.extraData,
type: r.type,
createdAt: r.createdAt,
userId: r.userId,
user: _userFromUserRow(u),
messageId: r.messageId,
score: r.score,
);
}
Future<List<Reaction>> _getOwnReactions(_Message messageRow) async {
return await (select(reactions).join([
leftOuterJoin(users, reactions.userId.equalsExp(users.id)),
])
..where(reactions.userId.equals(_userId))
..where(reactions.messageId.equals(messageRow.id))
..orderBy([OrderingTerm.asc(reactions.createdAt)]))
.map((row) {
final r = row.readTable(reactions);
final u = row.readTable(users);
return _reactionFromRow(r, u);
}).get();
}
Future<List<Read>> _getChannelReads(_Channel channelRow) async {
final rowReads = await (select(reads).join([
leftOuterJoin(users, reads.userId.equalsExp(users.id)),
])
..where(reads.channelCid.equals(channelRow.cid))
..orderBy([
OrderingTerm.asc(reads.lastRead),
]))
.map((row) {
final userRow = row.readTable(users);
final readRow = row.readTable(reads);
return Read(
user: _userFromUserRow(userRow),
lastRead: readRow.lastRead,
unreadMessages: readRow.unreadMessages,
);
}).get();
return rowReads;
}
Future<List<Member>> _getChannelMembers(_Channel channelRow) async {
final rowMembers = await (select(members).join([
leftOuterJoin(users, members.userId.equalsExp(users.id)),
])
..where(members.channelCid.equals(channelRow.cid))
..orderBy([
OrderingTerm.asc(members.createdAt),
]))
.map((row) {
final userRow = row.readTable(users);
final memberRow = row.readTable(members);
return Member(
user: _userFromUserRow(userRow),
userId: userRow.id,
banned: memberRow.banned,
shadowBanned: memberRow.shadowBanned,
updatedAt: memberRow.updatedAt,
createdAt: memberRow.createdAt,
role: memberRow.role,
inviteAcceptedAt: memberRow.inviteAcceptedAt,
invited: memberRow.invited,
inviteRejectedAt: memberRow.inviteRejectedAt,
isModerator: memberRow.isModerator,
);
}).get();
return rowMembers;
}
User _userFromUserRow(_User userRow) {
return User(
updatedAt: userRow.updatedAt,
role: userRow.role,
online: userRow.online,
lastActive: userRow.lastActive,
extraData: userRow.extraData,
banned: userRow.banned,
createdAt: userRow.createdAt,
id: userRow.id,
);
}
void _updateChannels(Batch batch, List<ChannelState> channelStates) {
batch.insertAll(
channels,
channelStates.map((cs) {
final channel = cs.channel;
return _channelDataFromChannelModel(channel);
}).toList(),
mode: InsertMode.insertOrReplace,
);
}
void _updateMembers(List<ChannelState> channelStates, Batch batch) async {
await (delete(members)
..where((tbl) =>
tbl.channelCid.isIn(channelStates.map((e) => e.channel.cid))))
.go();
final newMembers = channelStates
.map((cs) => cs.members.map((m) => _Member(
userId: m.user.id,
banned: m.banned,
shadowBanned: m.shadowBanned,
channelCid: cs.channel.cid,
createdAt: m.createdAt,
isModerator: m.isModerator,
inviteRejectedAt: m.inviteRejectedAt,
invited: m.invited,
inviteAcceptedAt: m.inviteAcceptedAt,
role: m.role,
updatedAt: m.updatedAt,
)))
.where((v) => v != null)
.expand((v) => v);
if (newMembers != null && newMembers.isNotEmpty) {
batch.insertAll(
members,
newMembers.toList(),
mode: InsertMode.insertOrReplace,
);
}
}
void _updateReads(List<ChannelState> channelStates, Batch batch) {
final newReads = channelStates
.map((cs) => cs.read?.map((r) => _Read(
lastRead: r.lastRead,
userId: r.user.id,
channelCid: cs.channel.cid,
unreadMessages: r.unreadMessages,
)))
.where((v) => v != null)
.expand((v) => v);
if (newReads != null && newReads.isNotEmpty) {
batch.insertAll(
reads,
newReads.toList(),
mode: InsertMode.insertOrReplace,
);
}
}
void _updateUsers(Batch batch, List<ChannelState> channelStates) {
batch.insertAll(
users,
channelStates
.map((cs) => [
if (cs.channel.createdBy != null)
_userDataFromUser(cs.channel.createdBy),
if (cs.messages != null)
...cs.messages
.map((m) => [
_userDataFromUser(m.user),
if (m.latestReactions != null)
...m.latestReactions
.where((r) => r.user != null)
.map((r) => _userDataFromUser(r.user)),
if (m.ownReactions != null)
...m.ownReactions
.where((r) => r.user != null)
.map((r) => _userDataFromUser(r.user)),
])
.expand((v) => v),
if (cs.read != null)
...cs.read.map((r) => _userDataFromUser(r.user)),
if (cs.members != null)
...cs.members.map((m) => _userDataFromUser(m.user)),
])
.expand((v) => v)
.toList(),
mode: InsertMode.insertOrReplace,
);
}
void _updateReactions(Batch batch, List<ChannelState> channelStates) {
batch.deleteWhere<_Reactions, _Reaction>(
reactions,
(r) => r.messageId.isIn(channelStates
.map((cs) => cs.messages.map((m) => m.id))
.expand((v) => v)),
);
final newReactions = channelStates
.map((cs) => cs.messages.map((m) {
final ownReactions =
m.ownReactions?.where((e) => e.userId != null)?.map(
(r) => _reactionDataFromReaction(m, r),
) ??
[];
final latestReactions =
m.latestReactions?.where((e) => e.userId != null)?.map(
(r) => _reactionDataFromReaction(m, r),
) ??
[];
return [
...ownReactions,
...latestReactions,
];
}).expand((v) => v))
.expand((v) => v);
if (newReactions.isNotEmpty) {
batch.insertAll(
reactions,
newReactions.toList(),
mode: InsertMode.insertOrReplace,
);
}
}
_Reaction _reactionDataFromReaction(Message m, Reaction r) {
return _Reaction(
messageId: m.id,
type: r.type,
extraData: r.extraData,
score: r.score,
createdAt: r.createdAt,
userId: r.userId,
);
}
_User _userDataFromUser(User user) {
return _User(
id: user.id,
createdAt: user.createdAt,
banned: user.banned,
extraData: user.extraData,
lastActive: user.lastActive,
online: user.online,
role: user.role,
updatedAt: user.updatedAt,
);
}
_Channel _channelDataFromChannelModel(ChannelModel channel) {
return _Channel(
id: channel.id,
config: jsonEncode(channel.config?.toJson() ?? {}),
type: channel.type,
frozen: channel.frozen,
createdAt: channel.createdAt,
updatedAt: channel.updatedAt,
memberCount: channel.memberCount,
cid: channel.cid,
lastMessageAt: channel.lastMessageAt,
deletedAt: channel.deletedAt,
extraData: channel.extraData,
createdBy: channel.createdBy?.id,
);
}
}
@@ -1,65 +0,0 @@
//ignore_for_file: public_member_api_docs
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:moor/ffi.dart';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/src/db/offline_storage.dart';
class SharedDB {
static Future<VmDatabase> constructDatabase(dbName) async {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, dbName);
final file = File(path);
return VmDatabase(file);
}
static Future<MoorIsolate> createMoorIsolate(String userId) async {
WidgetsFlutterBinding.ensureInitialized();
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'db_$userId.sqlite');
final receivePort = ReceivePort();
await Isolate.spawn(
startBackground,
_IsolateStartRequest(receivePort.sendPort, path),
);
return (await receivePort.first as MoorIsolate);
}
static void startBackground(_IsolateStartRequest request) {
final executor = LazyDatabase(() async {
return VmDatabase(File(request.targetPath));
});
final moorIsolate = MoorIsolate.inCurrent(
() => DatabaseConnection.fromExecutor(executor),
);
request.sendMoorIsolate.send(moorIsolate);
}
static Future<OfflineStorage> constructOfflineStorage({
userId,
logger,
}) async {
logger.info('Connecting on background isolate');
final isolate = await createMoorIsolate(userId);
final connection = await isolate.connect();
return OfflineStorage.connect(
connection,
userId,
isolate,
logger,
);
}
}
class _IsolateStartRequest {
final SendPort sendMoorIsolate;
final String targetPath;
_IsolateStartRequest(this.sendMoorIsolate, this.targetPath);
}
@@ -1,14 +0,0 @@
//ignore_for_file: public_member_api_docs
//ignore_for_file: always_declare_return_types
class SharedDB {
static constructDatabase(dbName) async {
print('Unsupported Platform');
return null;
}
static createMoorIsolate(userId) {}
static startBackground(request) {}
static constructOfflineStorage({userId, logger}) {}
}
@@ -1,20 +0,0 @@
//ignore_for_file: public_member_api_docs
//ignore_for_file: always_declare_return_types
import 'package:moor/moor_web.dart';
import 'package:stream_chat/src/db/offline_storage.dart';
class SharedDB {
static constructDatabase(dbName) async {
return WebDatabase(dbName);
}
static Future<OfflineStorage> constructOfflineStorage({
userId,
logger,
}) async {
return OfflineStorage(
userId,
logger,
);
}
}
@@ -10,25 +10,25 @@ part 'message.g.dart';
/// Enum defining the status of a sending message
enum MessageSendingStatus {
/// Message is being sent
SENDING,
sending,
/// Message is being updated
UPDATING,
updating,
/// Message is being deleted
DELETING,
deleting,
/// Message failed to send
FAILED,
failed,
/// Message failed to updated
FAILED_UPDATE,
failed_update,
/// Message failed to delete
FAILED_DELETE,
failed_delete,
/// Message correctly sent
SENT,
sent,
}
/// The class that contains the information about a message
@@ -72,7 +72,7 @@ class Message {
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final List<Reaction> ownReactions;
/// The ID of the parent message, if the message is a reply.
/// The ID of the parent message, if the message is a thread reply.
final String parentId;
/// A quoted reply message
@@ -186,7 +186,7 @@ class Message {
this.user,
this.extraData,
this.deletedAt,
this.status = MessageSendingStatus.SENT,
this.status = MessageSendingStatus.sent,
});
/// Create a new instance from a json
@@ -29,7 +29,7 @@ class Reaction {
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final String userId;
/// The score of the reaction (ie. number of reactions sent)
/// Reaction custom extraData
@JsonKey(includeIfNull: false)
final Map<String, dynamic> extraData;
@@ -1,119 +0,0 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stream_chat/src/api/responses.dart';
import 'package:stream_chat/src/db/offline_storage.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/message.dart';
import 'client.dart';
import 'models/own_user.dart';
/// Utility class to handle and show notifications
class NotificationService {
static Future<void> _handleNotification(
Message message,
ChannelModel channelModel,
Client client,
) async {
if (message != null && client.persistenceEnabled) {
if (client?.state?.channels == null) {
final sharedPreferences = await _getSharedPreferences();
final userId = sharedPreferences.getString(KEY_USER_ID);
final offlineStorage = OfflineStorage(userId, Logger('💽'));
await offlineStorage.updateChannelState(
ChannelState(
channel: channelModel,
messages: [message],
),
);
await offlineStorage.disconnect();
} else {
final channel = client.state.channels[channelModel.cid];
channel.state.updateChannelState(
ChannelState(
channel: channelModel,
messages: [message],
),
);
}
}
}
static SharedPreferences _sharedPreferences;
static Future<SharedPreferences> _getSharedPreferences() async {
_sharedPreferences ??= await SharedPreferences.getInstance();
return _sharedPreferences;
}
/// Gets the message using the client without storing it in the offline storage
/// It returns an object containing the information about the message and the channel
static Future<GetMessageResponse> getMessage(String messageId) async {
final sharedPreferences = await _getSharedPreferences();
final apiKey = sharedPreferences.getString(KEY_API_KEY);
final client = Client(
apiKey,
persistenceEnabled: false,
);
final userId = sharedPreferences.getString(KEY_USER_ID);
final token = sharedPreferences.getString(KEY_TOKEN);
client.state.user = OwnUser(id: userId);
client.token = token;
final res = await client.getMessage(messageId);
return res;
}
/// Stores the message in the offline storage
static Future<void> storeMessage(GetMessageResponse messageResponse) async {
final sharedPreferences = await _getSharedPreferences();
final userId = sharedPreferences.getString(KEY_USER_ID);
final offlineStorage = OfflineStorage(
userId,
Logger('💽'),
);
await offlineStorage.updateChannelState(ChannelState(
messages: [messageResponse.message],
channel: messageResponse.channel,
));
await offlineStorage.disconnect();
}
/// Gets the message using the client and stores it in the offline storage
/// It returns an object containing the information about the message and the channel
static Future<GetMessageResponse> getAndStoreMessage(String messageId) async {
final getMessageResponse = await getMessage(messageId);
await storeMessage(getMessageResponse);
return getMessageResponse;
}
/// Handles the ios message queue generated by the Notification Service Extension
static Future<void> handleIosMessageQueue(Client client) async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.reload();
final messageQueue = sharedPreferences.getStringList('messageQueue');
if (messageQueue != null) {
messageQueue.forEach((m) {
final jsonMessage = jsonDecode(m);
final message = Message.fromJson(jsonMessage);
final channelModel = ChannelModel.fromJson(jsonMessage['channel']);
_handleNotification(
message,
channelModel,
client,
);
});
await sharedPreferences.remove('messageQueue');
}
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ library stream_chat;
export 'package:dio/src/dio_error.dart';
export 'package:dio/src/multipart_file.dart';
export 'package:logging/src/level.dart';
export 'package:logging/logging.dart' show Logger, Level;
export './src/api/channel.dart';
export './src/api/connection_status.dart';
@@ -26,4 +26,4 @@ export './src/models/own_user.dart';
export './src/models/reaction.dart';
export './src/models/read.dart';
export './src/models/user.dart';
export './src/notifications.dart';
export './src/db/chat_persistence_client.dart';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:stream_chat/src/client.dart';
/// Current package version
/// Used in [Client] to build the `x-stream-client` header
/// Used in [StreamChatClient] to build the `x-stream-client` header
const PACKAGE_VERSION = '0.2.24+2';
+2 -12
View File
@@ -9,28 +9,18 @@ environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
json_annotation: ^3.0.1
shared_preferences: ^0.5.7+3
logging: ^0.11.4
dio: ^3.0.10
web_socket_channel: ^1.1.0
uuid: ^2.2.2
async: ^2.4.1
stream_channel: ^2.0.0
moor: ^3.3.1
path_provider: ^1.6.10
path: ^1.6.4
rxdart: ^0.24.1
collection: ^1.14.12
sqlite3_flutter_libs: ^0.3.0
pedantic: ^1.9.2
dev_dependencies:
build_runner: ^1.10.0
json_serializable: ^3.3.0
moor_generator: ^3.1.0
flutter_test:
sdk: flutter
test: ^1.15.7
mockito: ^4.1.1
pedantic: ^1.9.2
@@ -1,6 +1,5 @@
import 'package:dio/dio.dart';
import 'package:dio/native_imp.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/src/api/requests.dart';
import 'package:stream_chat/src/client.dart';
@@ -8,6 +7,7 @@ import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/reaction.dart';
import 'package:test/test.dart';
class MockDio extends Mock implements DioForNative {}
@@ -22,7 +22,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -51,7 +51,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -83,7 +83,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -108,7 +108,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -149,7 +149,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -173,7 +173,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -198,7 +198,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -223,7 +223,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -247,7 +247,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -272,7 +272,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -306,7 +306,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -340,7 +340,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -375,7 +375,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -414,7 +414,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -448,7 +448,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -475,7 +475,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -501,7 +501,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -527,7 +527,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -845,7 +845,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1159,7 +1159,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1474,7 +1474,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1789,7 +1789,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1815,7 +1815,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1842,7 +1842,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1863,7 +1863,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1885,7 +1885,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1910,7 +1910,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1935,7 +1935,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1961,7 +1961,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1993,7 +1993,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -2024,7 +2024,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -2064,7 +2064,7 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
@@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.dart';
void main() {
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/api/responses.dart';
import 'package:stream_chat/src/models/device.dart';
import 'package:stream_chat/src/models/member.dart';
@@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/api/web_socket_channel_stub.dart';
void main() {
@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:logging/logging.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/src/api/connection_status.dart';
@@ -67,7 +67,7 @@ void main() {
await ws.connect();
verify(connectFunc(computedUrl)).called(1);
expect(ws.connectionStatus.value, ConnectionStatus.connected);
expect(ws.connectionStatus, ConnectionStatus.connected);
await streamController.close();
timer.cancel();
+39 -83
View File
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:dio/native_imp.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:logging/logging.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/src/api/requests.dart';
@@ -11,6 +10,7 @@ import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:test/test.dart';
class MockDio extends Mock implements DioForNative {}
@@ -41,10 +41,7 @@ void main() {
});
test('should create the object correctly', () {
final client = Client(
'api-key',
persistenceEnabled: false,
);
final client = StreamChatClient('api-key');
expect(client.baseURL, 'chat-us-east-1.stream-io-api.com');
expect(client.apiKey, 'api-key');
@@ -58,9 +55,8 @@ void main() {
print(record.message);
};
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 12),
logLevel: Level.INFO,
@@ -82,10 +78,7 @@ void main() {
}));
test('Channel', () {
final client = Client(
'test',
persistenceEnabled: false,
);
final client = StreamChatClient('test');
final Map<String, dynamic> data = {'test': 1};
final channelClient = client.channel('type', id: 'id', extraData: data);
expect(channelClient.type, 'type');
@@ -100,10 +93,9 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
persistenceEnabled: false,
);
final queryParams = {
@@ -132,10 +124,9 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
httpClient: mockDio,
persistenceEnabled: false,
);
final queryFilter = <String, dynamic>{
@@ -184,9 +175,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -213,9 +203,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -258,9 +247,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -285,9 +273,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -305,9 +292,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -323,10 +309,7 @@ void main() {
});
test('devToken', () {
final client = Client(
'api-key',
persistenceEnabled: false,
);
final client = StreamChatClient('api-key');
final token = client.devToken('test');
expect(
@@ -342,9 +325,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -371,9 +353,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -415,9 +396,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -437,9 +417,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -453,9 +432,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -475,9 +453,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -501,9 +478,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -531,9 +507,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -553,9 +528,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -576,9 +550,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -598,9 +571,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -622,9 +594,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -644,9 +615,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -666,9 +636,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -694,9 +663,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -716,9 +684,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -738,9 +705,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -761,9 +727,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -787,9 +752,8 @@ void main() {
final mockHttpClientAdapter = MockHttpClientAdapter();
dioHttp.httpClientAdapter = mockHttpClientAdapter;
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: dioHttp,
);
@@ -807,9 +771,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -831,9 +794,8 @@ void main() {
final mockHttpClientAdapter = MockHttpClientAdapter();
dioHttp.httpClientAdapter = mockHttpClientAdapter;
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: dioHttp,
);
@@ -851,9 +813,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -875,9 +836,8 @@ void main() {
final mockHttpClientAdapter = MockHttpClientAdapter();
dioHttp.httpClientAdapter = mockHttpClientAdapter;
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: dioHttp,
);
@@ -895,9 +855,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -920,9 +879,8 @@ void main() {
final mockHttpClientAdapter = MockHttpClientAdapter();
dioHttp.httpClientAdapter = mockHttpClientAdapter;
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: dioHttp,
);
@@ -940,9 +898,8 @@ void main() {
when(mockDio.options).thenReturn(BaseOptions());
when(mockDio.interceptors).thenReturn(Interceptors());
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: mockDio,
);
@@ -966,9 +923,8 @@ void main() {
final mockHttpClientAdapter = MockHttpClientAdapter();
dioHttp.httpClientAdapter = mockHttpClientAdapter;
final client = Client(
final client = StreamChatClient(
'api-key',
persistenceEnabled: false,
httpClient: dioHttp,
);
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/action.dart';
void main() {
@@ -2,7 +2,7 @@ import 'package:stream_chat/src/models/attachment.dart';
import 'package:stream_chat/src/models/action.dart';
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
void main() {
group('src/models/attachment', () {
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/channel_config.dart';
import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/command.dart';
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/channel_model.dart';
void main() {
@@ -1,7 +1,7 @@
import 'package:stream_chat/src/models/command.dart';
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
void main() {
group('src/models/command', () {
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/device.dart';
void main() {
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/own_user.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/member.dart';
import 'package:stream_chat/src/models/user.dart';
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/attachment.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/reaction.dart';
@@ -116,7 +116,7 @@ void main() {
showInChannel: true,
parentId: 'parentId',
extraData: {'hey': 'test'},
status: MessageSendingStatus.SENT,
status: MessageSendingStatus.sent,
);
expect(
@@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/reaction.dart';
import 'dart:convert';
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/read.dart';
import 'package:stream_chat/src/models/user.dart';
@@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/serialization.dart';
void main() {
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/user.dart';
void main() {
+1 -1
View File
@@ -1,6 +1,6 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:test/test.dart';
import 'package:stream_chat/version.dart';
void prepareTest() {
@@ -1,21 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:logging/logging.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/stream_chat.dart';
const API_KEY = '6xjf3dex3n7d';
const TOKEN =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo';
void main() {
test('test', () async {
final client = Client(
'6xjf3dex3n7d',
logLevel: Level.INFO,
tokenProvider: (_) async => '',
);
final user = User(id: 'wild-breeze-7');
await client.setGuestUser(user);
});
}
+3 -3
View File
@@ -1,7 +1,7 @@
# Official Flutter SDK for [Stream Chat](https://getstream.io/chat/)
<p align="center">
<a href="https://getstream.io/tutorials/ios-chat/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
<a href="https://getstream.io/chat/flutter/tutorial/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
</p>
> The official Flutter components for Stream Chat, a service for
@@ -91,7 +91,7 @@ Out of the box, all chat widgets use their default styling, and there are two wa
1. Initialize the `StreamChatTheme` from your existing `MaterialApp` style
```dart
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -117,7 +117,7 @@ Out of the box, all chat widgets use their default styling, and there are two wa
2. Construct a custom theme and provide all the customizations needed
```dart
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -1,17 +1,12 @@
import 'dart:io';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'choose_user_page.dart';
import 'notifications_service.dart';
import 'main.dart';
class AdvancedOptionsPage extends StatefulWidget {
@override
@@ -272,14 +267,10 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
),
);
final client = Client(
final client = StreamChatClient(
apiKey,
logLevel: Level.INFO,
showLocalNotification: (!kIsWeb && Platform.isAndroid)
? showLocalNotification
: null,
persistenceEnabled: true,
);
)..chatPersistenceClient = chatPersistentClient;
try {
await client.setUser(
@@ -289,10 +280,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
userToken,
);
if (!kIsWeb) {
initNotifications(client);
}
final secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
@@ -319,73 +306,13 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
await client.disconnect();
return;
}
if (!kIsWeb) {
initNotifications(client);
}
Navigator.pop(context);
Navigator.pop(context);
await Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) {
return FutureBuilder<StreamingSharedPreferences>(
future: StreamingSharedPreferences.instance,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
return PreferenceBuilder<int>(
preference: snapshot.data.getInt(
'theme',
defaultValue: 0,
),
builder: (context, snapshot) => MaterialApp(
builder: (context, child) {
return StreamChat(
client: client,
child: Builder(
builder: (context) =>
AnnotatedRegion<
SystemUiOverlayStyle>(
child: child,
value: SystemUiOverlayStyle(
systemNavigationBarColor:
StreamChatTheme.of(context)
.colorTheme
.white,
systemNavigationBarIconBrightness:
Theme.of(context)
.brightness ==
Brightness.dark
? Brightness.light
: Brightness.dark,
),
),
),
);
},
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: {
-1: ThemeMode.dark,
0: ThemeMode.system,
1: ThemeMode.light,
}[snapshot],
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: client.state.user == null
? Routes.CHOOSE_USER
: Routes.HOME,
),
);
},
);
},
),
);
loading = false;
await Navigator.pushNamedAndRemoveUntil(
context,
Routes.APP,
ModalRoute.withName(Routes.APP),
arguments: client,
);
}
},
),
@@ -1,11 +1,9 @@
import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'notifications_service.dart';
import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY';
@@ -187,10 +185,6 @@ class ChooseUserPage extends StatelessWidget {
key: kStreamToken,
value: token,
);
if (!kIsWeb) {
initNotifications(client);
}
Navigator.pushNamedAndRemoveUntil(
context,
Routes.HOME,
@@ -15,7 +15,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly
/// or to retrieve outer scope needed such as messages from the [Channel.state].
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -29,7 +29,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -19,7 +19,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// We also change the message color posted by the current user.
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -33,7 +33,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -20,7 +20,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// - We retrieve the count of unread messages from [Channel.state]
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -34,7 +34,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -15,7 +15,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly
/// or to retrieve outer scope needed such as messages from the [Channel.state].
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -29,7 +29,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -150,116 +150,116 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
),
],
),
body: ValueListenableBuilder<ConnectionStatus>(
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
builder: (context, status, _) {
String statusString = '';
bool showStatus = true;
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey,
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: UserAvatar(
user: user,
constraints: BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: UserAvatar(
user: user,
constraints: BoxConstraints.tightFor(
width: 40,
height: 40,
),
.black,
),
title: Text(
user.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: StreamChatTheme.of(context)
.colorTheme
.black,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
}
},
),
);
},
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
}
},
),
);
},
),
),
],
),
);
}),
),
],
),
);
},
),
),
);
}
+10 -10
View File
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:example/chat_info_screen.dart';
import 'package:example/choose_user_page.dart';
@@ -10,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'notifications_service.dart';
@@ -17,6 +17,11 @@ import 'routes/app_routes.dart';
import 'routes/routes.dart';
import 'search_text_field.dart';
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
connectionMode: ConnectionMode.background,
);
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final secureStorage = FlutterSecureStorage();
@@ -24,13 +29,10 @@ void main() async {
final apiKey = await secureStorage.read(key: kStreamApiKey);
final userId = await secureStorage.read(key: kStreamUserId);
final client = Client(
final client = StreamChatClient(
apiKey ?? kDefaultStreamApiKey,
logLevel: Level.INFO,
showLocalNotification:
(!kIsWeb && Platform.isAndroid) ? showLocalNotification : null,
persistenceEnabled: true,
);
)..chatPersistenceClient = chatPersistentClient;
if (userId != null) {
final token = await secureStorage.read(key: kStreamToken);
@@ -38,16 +40,13 @@ void main() async {
User(id: userId),
token,
);
if (!kIsWeb) {
initNotifications(client);
}
}
runApp(MyApp(client));
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -68,6 +67,7 @@ class MyApp extends StatelessWidget {
builder: (context, child) {
return StreamChat(
client: client,
onBackgroundEventReceived: showLocalNotification,
child: Builder(
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>(
child: child,
@@ -19,7 +19,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// In this case we are showing the list of channels the current user is a member and we order them based on the time they had a new message.
/// [ChannelListView] handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel.
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -33,7 +33,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -134,9 +134,8 @@ class _NewChatScreenState extends State<NewChatScreen> {
),
centerTitle: true,
),
body: ValueListenableBuilder<ConnectionStatus>(
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
builder: (context, status, _) {
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
@@ -87,227 +87,225 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
)
],
),
body: ValueListenableBuilder<ConnectionStatus>(
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
builder: (context, status, _) {
String statusString = '';
bool showStatus = true;
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
),
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
Stack(
children: [
UserAvatar(
onlineIndicatorAlignment:
Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
Stack(
children: [
UserAvatar(
onlineIndicatorAlignment:
Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(() =>
_selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(() =>
_selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.white,
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.white,
shape: BoxShape.circle,
border: Border.all(
color:
StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
),
),
child: StreamSvgIcon.close(
color: StreamChatTheme.of(context)
.colorTheme
.black,
size: 24,
.whiteSnow,
),
),
),
)
],
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context)
.colorTheme
.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? 'Matches for \"$_userNameQuery\"'
: 'On the platform',
style: TextStyle(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
pullToRefresh: false,
groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) {
if (!_selectedUsers.contains(user)) {
setState(() {
_selectedUsers.add(user);
});
} else {
setState(() {
_selectedUsers.remove(user);
});
}
},
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_userNameQuery.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
}
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: StreamChatTheme.of(context)
.colorTheme
.grey,
child: StreamSvgIcon.close(
color: StreamChatTheme.of(context)
.colorTheme
.black,
size: 24,
),
),
),
Text(
'No user matches these keywords...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
],
)
],
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
);
},
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? 'Matches for \"$_userNameQuery\"'
: 'On the platform',
style: TextStyle(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
pullToRefresh: false,
groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) {
if (!_selectedUsers.contains(user)) {
setState(() {
_selectedUsers.add(user);
});
} else {
setState(() {
_selectedUsers.remove(user);
});
}
},
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_userNameQuery.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
}
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
Text(
'No user matches these keywords...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
],
),
),
),
);
},
);
},
),
),
),
);
}),
),
);
},
),
);
}
}
@@ -1,11 +1,9 @@
import 'dart:io';
import 'package:flutter_apns/flutter_apns.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message;
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void showLocalNotification(Message message, ChannelModel channel) async {
void showLocalNotification(Event event) async {
if (event.message == null) return;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final initializationSettingsAndroid =
AndroidInitializationSettings('launch_background');
@@ -16,9 +14,9 @@ void showLocalNotification(Message message, ChannelModel channel) async {
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin.show(
message.id.hashCode,
'${message.user.name} @ ${channel.name}',
message.text,
event.message.id.hashCode,
event.message.user.name,
event.message.text,
NotificationDetails(
android: AndroidNotificationDetails(
'message channel',
@@ -31,33 +29,3 @@ void showLocalNotification(Message message, ChannelModel channel) async {
),
);
}
Future backgroundHandler(Map<String, dynamic> notification) async {
print('new notification ${notification}');
final messageId = notification['data']['id'];
final notificationData =
await NotificationService.getAndStoreMessage(messageId);
showLocalNotification(
notificationData.message,
notificationData.channel,
);
}
void initNotifications(Client client) {
final connector = createPushConnector();
connector.configure(
onBackgroundMessage: backgroundHandler,
);
connector.requestNotificationPermissions();
connector.token.addListener(() {
if (connector.token.value != null) {
client.addDevice(
connector.token.value,
Platform.isAndroid ? PushProvider.firebase : PushProvider.apn,
);
}
});
}
@@ -15,6 +15,12 @@ class AppRoutes {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case Routes.APP:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.APP),
builder: (_) {
return MyApp(args);
});
case Routes.HOME:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.HOME),
@@ -1,5 +1,6 @@
/// Define all the route names here
class Routes {
static const String APP = '/app';
static const String HOME = '/home';
static const String CHOOSE_USER = '/choose_user';
static const String ADVANCED_OPTIONS = '/advance_options';
@@ -5,8 +5,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// There are three important things to notice that are common to all Flutter application using StreamChat:
///
/// 1. The Dart API [Client] is initialized with your API Key
/// 2. The current user is set by calling [Client.setUser]
/// 1. The Dart API [StreamChatClient] is initialized with your API Key
/// 2. The current user is set by calling [StreamChatClient.setUser]
/// 3. The client is then passed to the top-level [StreamChat] widget
/// [StreamChat] is an inherited widget and must be the parent of all Chat related widgets.
///
@@ -15,9 +15,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// Let's have a look at what we've built:
///
/// - We set up the Chat [Client] with the API key
/// - We set up the Chat [StreamChatClient] with the API key
///
/// - We set the the current user for Chat with [Client.setUser] and a pre-generated user token
/// - We set the the current user for Chat with [StreamChatClient.setUser] and a pre-generated user token
///
/// - We make [StreamChat] the root Widget of our application
///
@@ -25,7 +25,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// If you now run the simulator you will see a single channel UI.
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -44,7 +44,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
final Channel channel;
MyApp(this.client, this.channel);
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -16,7 +16,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -10,7 +10,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same [ThreadPage].
void main() async {
final client = Client(
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -24,7 +24,7 @@ void main() async {
}
class MyApp extends StatelessWidget {
final Client client;
final StreamChatClient client;
MyApp(this.client);
@@ -9,8 +9,10 @@ environment:
dependencies:
flutter:
sdk: flutter
stream_chat_flutter:
stream_chat_flutter:
path: ../
stream_chat_persistence:
path: ../../stream_chat_persistence
flutter_apns: ^1.4.1
flutter_local_notifications: ^2.0.2
flutter_svg: ^0.19.1
@@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/back_button.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/channel_name.dart';
import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import '../stream_chat_flutter.dart';
import './channel_name.dart';
import '../stream_chat_flutter.dart';
import 'channel_image.dart';
import 'connection_status_builder.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
@@ -17,7 +18,7 @@ import 'channel_image.dart';
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final StreamChatClient client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
@@ -85,11 +86,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
final _client = StreamChat.of(context).client;
return ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, _) {
return ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
@@ -1,8 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/group_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png)
@@ -11,7 +11,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final StreamChatClient client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'connection_status_builder.dart';
class ChannelInfo extends StatelessWidget {
final Channel channel;
@@ -25,9 +27,8 @@ class ChannelInfo extends StatelessWidget {
stream: channel.state.membersStream,
initialData: channel.state.members,
builder: (context, snapshot) {
return ValueListenableBuilder(
valueListenable: client.wsConnectionStatus,
builder: (context, status, child) {
return ConnectionStatusBuilder(
statusBuilder: (context, status) {
switch (status) {
case ConnectionStatus.connected:
return _buildConnectedTitleState(context, snapshot.data);
@@ -110,7 +111,8 @@ class ChannelInfo extends StatelessWidget {
);
}
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
Widget _buildDisconnectedTitleState(
BuildContext context, StreamChatClient client) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -2,25 +2,26 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'connection_status_builder.dart';
import 'info_tile.dart';
import 'stream_chat.dart';
typedef _TitleBuilder = Widget Function(
BuildContext context,
ConnectionStatus status,
Client client,
StreamChatClient client,
);
///
/// It shows the current [Client] status.
/// It shows the current [StreamChatClient] status.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final StreamChatClient client;
///
/// MyApp(this.client);
///
@@ -41,8 +42,8 @@ typedef _TitleBuilder = Widget Function(
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
///
/// The widget by default uses the inherited [Client] to fetch information about the status.
/// However you can also pass your own [Client] if you don't have it in the widget tree.
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
@@ -58,8 +59,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
this.preNavigationCallback,
}) : super(key: key);
/// Pass this if you don't have a [Client] in your widget tree.
final Client client;
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
final StreamChatClient client;
/// Use this to build your own title as per different [ConnectionStatus]
final _TitleBuilder titleBuilder;
@@ -79,9 +80,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
final user = _client.state.user;
return ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, child) {
return ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
@@ -130,9 +130,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
actions: [
StreamNeumorphicButton(
child: IconButton(
icon: ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, child) {
icon: ConnectionStatusBuilder(
statusBuilder: (context, status) {
var color;
switch (status) {
case ConnectionStatus.connected:
@@ -216,7 +215,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
);
}
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
Widget _buildDisconnectedTitleState(
BuildContext context, StreamChatClient client) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'stream_chat.dart';
/// Widget that builds itself based on the latest snapshot of interaction with
/// a [Stream] of type [ConnectionStatus].
///
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] in case no
/// stream is provided.
class ConnectionStatusBuilder extends StatelessWidget {
/// Creates a new ConnectionStatusBuilder
const ConnectionStatusBuilder({
Key key,
@required this.statusBuilder,
this.initialStatus = ConnectionStatus.disconnected,
this.connectionStatusStream,
this.errorBuilder,
this.loadingBuilder,
}) : assert(statusBuilder != null),
super(key: key);
/// The connection status that will be used to create the initial snapshot.
final ConnectionStatus initialStatus;
/// The asynchronous computation to which this builder is currently connected.
final Stream<ConnectionStatus> connectionStatusStream;
/// The builder that will be used in case of error
final Widget Function(BuildContext context, Object error) errorBuilder;
/// The builder that will be used in case of loading
final WidgetBuilder loadingBuilder;
/// The builder that will be used in case of data
final Widget Function(BuildContext context, ConnectionStatus status)
statusBuilder;
@override
Widget build(BuildContext context) {
final stream = connectionStatusStream ??
StreamChat.of(context).client.wsConnectionStatusStream;
return StreamBuilder<ConnectionStatus>(
initialData: initialStatus,
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (errorBuilder != null) {
return errorBuilder(context, snapshot.error);
}
return Offstage();
}
if (!snapshot.hasData) {
if (loadingBuilder != null) return loadingBuilder(context);
return Offstage();
}
return statusBuilder(context, snapshot.data);
},
);
}
}
@@ -126,7 +126,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
children: <Widget>[
if (widget.showReactions &&
(widget.message.status ==
MessageSendingStatus.SENT ||
MessageSendingStatus.sent ||
widget.message.status == null))
Align(
alignment: Alignment(
@@ -174,7 +174,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showReactionPickerIndicator:
widget.showReactions &&
(widget.message.status ==
MessageSendingStatus.SENT ||
MessageSendingStatus.sent ||
widget.message.status == null),
showInChannelIndicator: false,
showSendingIndicator: false,
@@ -203,13 +203,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
children: [
if (widget.showReplyMessage &&
(widget.message.status ==
MessageSendingStatus.SENT ||
MessageSendingStatus.sent ||
widget.message.status == null) &&
widget.message.parentId == null)
_buildReplyButton(context),
if (widget.showThreadReplyMessage &&
(widget.message.status ==
MessageSendingStatus.SENT ||
MessageSendingStatus.sent ||
widget.message.status == null) &&
widget.message.parentId == null)
_buildThreadReplyButton(context),
@@ -486,7 +486,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed =
widget.message.status == MessageSendingStatus.FAILED_DELETE;
widget.message.status == MessageSendingStatus.failed_delete;
return InkWell(
onTap: () => _showDeleteDialog(),
child: Padding(
@@ -561,7 +561,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed =
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
widget.message.status == MessageSendingStatus.failed_update;
return InkWell(
onTap: () {
Navigator.pop(context);
@@ -2210,7 +2210,7 @@ class MessageInputState extends State<MessageInput> {
_mentionedUsers.clear();
if (widget.editMessage == null ||
widget.editMessage.status == MessageSendingStatus.FAILED) {
widget.editMessage.status == MessageSendingStatus.failed) {
sendingFuture = channel.sendMessage(message);
} else {
sendingFuture = StreamChat.of(context).client.updateMessage(
@@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart';
import 'connection_status_builder.dart';
import 'date_divider.dart';
import 'swipeable.dart';
import 'extension.dart';
@@ -301,197 +302,193 @@ class _MessageListViewState extends State<MessageListView> {
}
_messageListLength = newMessagesListLength;
final _client = StreamChat.of(context).client;
return Stack(
alignment: Alignment.center,
children: [
ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, _) {
String statusString = '';
bool showStatus = true;
ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: widget.showConnectionStateTile ? showStatus : false,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: LazyLoadScrollView(
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount:
messages.length + 2 + (_isThreadConversation ? 1 : 0),
separatorBuilder: (context, i) {
if (i == messages.length) return Offstage();
if (i == 0) return SizedBox(height: 30);
if (i == messages.length + 1) {
final replyCount = widget.parentMessage.replyCount;
return Container(
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
);
}
return InfoTile(
showMessage:
widget.showConnectionStateTile ? showStatus : false,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: LazyLoadScrollView(
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
final message = messages[i];
final nextMessage = messages[i - 1];
if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(),
Units.DAY,
)) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
nextMessage.createdAt.toLocal(),
)
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: divider,
);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
message.createdAt.toLocal(),
Units.MINUTE,
);
final isNextUserSame =
message.user.id == nextMessage.user?.id;
final isThread = message.replyCount > 0;
final isDeleted = message.isDeleted;
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return SizedBox(height: 8);
}
return SizedBox(height: 2);
},
itemBuilder: (context, i) {
if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context,
widget.parentMessage,
);
} else {
return buildParentMessage(widget.parentMessage);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount:
messages.length + 2 + (_isThreadConversation ? 1 : 0),
separatorBuilder: (context, i) {
if (i == messages.length) return Offstage();
if (i == 0) return SizedBox(height: 30);
if (i == messages.length + 1) {
final replyCount = widget.parentMessage.replyCount;
return Container(
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context)
.colorTheme
.bgGradient,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
);
}
final message = messages[i - 1];
final message = messages[i];
final nextMessage = messages[i - 1];
if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(),
Units.DAY,
)) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
nextMessage.createdAt.toLocal(),
)
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: divider,
);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
message.createdAt.toLocal(),
Units.MINUTE,
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
);
final isNextUserSame =
message.user.id == nextMessage.user?.id;
final isThread = message.replyCount > 0;
final isDeleted = message.isDeleted;
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return SizedBox(height: 8);
}
return SizedBox(height: 2);
},
itemBuilder: (context, i) {
if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context,
widget.parentMessage,
);
} else {
return buildParentMessage(widget.parentMessage);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
messageWidget = buildMessage(message, messages, i);
}
messageWidget = buildMessage(message, messages, i);
}
return messageWidget;
},
),
}
return messageWidget;
},
),
);
}),
),
);
},
),
if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned(
top: 20.0,
@@ -93,7 +93,7 @@ class MessageReactionsModal extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
(message.status == MessageSendingStatus.SENT ||
(message.status == MessageSendingStatus.sent ||
message.status == null))
Align(
alignment: Alignment(
@@ -141,7 +141,7 @@ class MessageReactionsModal extends StatelessWidget {
),
showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
MessageSendingStatus.sent ||
message.status == null),
),
),
@@ -263,13 +263,13 @@ class _MessageWidgetState extends State<MessageWidget> {
bool get hasQuotedMessage => widget.message?.quotedMessage != null;
bool get isSendFailed => widget.message.status == MessageSendingStatus.FAILED;
bool get isSendFailed => widget.message.status == MessageSendingStatus.failed;
bool get isUpdateFailed =>
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
widget.message.status == MessageSendingStatus.failed_update;
bool get isDeleteFailed =>
widget.message.status == MessageSendingStatus.FAILED_DELETE;
widget.message.status == MessageSendingStatus.failed_delete;
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
@@ -875,7 +875,7 @@ class _MessageWidgetState extends State<MessageWidget> {
void onLongPress(BuildContext context) {
if (widget.message.isEphemeral ||
widget.message.status == MessageSendingStatus.SENDING) {
widget.message.status == MessageSendingStatus.sending) {
return;
}
@@ -986,11 +986,11 @@ class _MessageWidgetState extends State<MessageWidget> {
void retryMessage(BuildContext context) {
final channel = StreamChannel.of(context).channel;
if (widget.message.status == MessageSendingStatus.FAILED) {
if (widget.message.status == MessageSendingStatus.failed) {
channel.sendMessage(widget.message);
return;
}
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
if (widget.message.status == MessageSendingStatus.failed_update) {
StreamChat.of(context).client.updateMessage(
widget.message,
channel.cid,
@@ -998,7 +998,7 @@ class _MessageWidgetState extends State<MessageWidget> {
return;
}
if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
if (widget.message.status == MessageSendingStatus.failed_delete) {
StreamChat.of(context).client.deleteMessage(
widget.message,
channel.cid,
@@ -22,14 +22,14 @@ class SendingIndicator extends StatelessWidget {
color: StreamChatTheme.of(context).colorTheme.accentBlue,
);
}
if (message.status == MessageSendingStatus.SENT || message.status == null) {
if (message.status == MessageSendingStatus.sent || message.status == null) {
return StreamSvgIcon.check(
size: size,
color: IconTheme.of(context).color.withOpacity(0.5),
);
}
if (message.status == MessageSendingStatus.SENDING ||
message.status == MessageSendingStatus.UPDATING) {
if (message.status == MessageSendingStatus.sending ||
message.status == MessageSendingStatus.updating) {
return Icon(
Icons.access_time,
size: size,
@@ -4,14 +4,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget used to provide information about the chat to the widget tree
///
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final StreamChatClient client;
///
/// MyApp(this.client);
///
@@ -30,15 +30,25 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
class StreamChat extends StatefulWidget {
final Client client;
final StreamChatClient client;
final Widget child;
final StreamChatThemeData streamChatThemeData;
/// The amount of time that will pass before disconnecting the client in the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
/// is in background. Can be used to display various notifications depending
/// upon the [Event.type]
final EventHandler onBackgroundEventReceived;
StreamChat({
Key key,
@required this.client,
@required this.child,
this.streamChatThemeData,
this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1),
}) : super(
key: key,
);
@@ -63,7 +73,7 @@ class StreamChat extends StatefulWidget {
/// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> {
Client get client => widget.client;
StreamChatClient get client => widget.client;
@override
Widget build(BuildContext context) {
@@ -82,8 +92,10 @@ class StreamChatState extends State<StreamChat> {
scaffoldBackgroundColor: streamTheme.colorTheme.white,
),
child: StreamChatCore(
child: widget.child,
client: client,
child: widget.child,
onBackgroundEventReceived: widget.onBackgroundEventReceived,
backgroundKeepAlive: widget.backgroundKeepAlive,
),
);
},
@@ -40,4 +40,5 @@ export 'src/channel_file_display_screen.dart';
export 'src/channel_media_display_screen.dart';
export 'src/info_tile.dart';
export 'src/stream_chat.dart';
export 'src/connection_status_builder.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
+1 -2
View File
@@ -58,8 +58,7 @@ flutter:
- animations/
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^4.1.3
pedantic: ^1.9.2
pedantic: ^1.9.2
+1 -1
View File
@@ -1,7 +1,7 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
class MockClient extends Mock implements Client {}
class MockClient extends Mock implements StreamChatClient {}
class MockClientState extends Mock implements ClientState {}
@@ -1,3 +1,3 @@
## 1.0.0-rc
## 1.0.0-beta
* First release
+1 -1
View File
@@ -1,7 +1,7 @@
# Official Flutter SDK Core for [Stream Chat](https://getstream.io/chat/)
<p align="center">
<a href="https://getstream.io/tutorials/ios-chat/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
<a href="https://getstream.io/chat/flutter/tutorial/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
</p>
> The official Flutter core components for Stream Chat, a service for
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Future<void> 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.
@@ -43,10 +43,10 @@ class StreamExample extends StatelessWidget {
}) : super(key: key);
/// Instance of Stream Client.
/// Stream's [Client] can be used to connect to our servers and set the default
/// Stream's [StreamChatClient] can be used to connect to our servers and set the default
/// user for the application. Performing these actions trigger a websocket connection
/// allowing for real-time updates.
final Client client;
final StreamChatClient client;
@override
Widget build(BuildContext context) {
@@ -287,8 +287,8 @@ class _MessageScreenState extends State<MessageScreen> {
}
/// Extensions can be used to add functionality to the SDK. In the examples
/// below, we add two simple extensions to the [Client] and [Channel].
extension on Client {
/// below, we add two simple extensions to the [StreamChatClient] and [Channel].
extension on StreamChatClient {
/// Fetches the current user id.
String get uid => state.user.id;
}
@@ -16,7 +16,6 @@ class LazyLoadScrollView extends StatefulWidget {
this.onPageScrollStart,
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.isLoading = false,
this.scrollOffset = 100,
}) : assert(child != null),
super(key: key);
@@ -42,9 +41,6 @@ class LazyLoadScrollView extends StatefulWidget {
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
final double scrollOffset;
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a [FutureBuilder] or [StreamBuilder].
final bool isLoading;
@override
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
}
@@ -59,7 +59,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [Client.search] updating [messageResponses] stream
/// Calls [StreamChatClient.search] updating [messageResponses] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -82,7 +82,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
}
}
/// Calls [Client.search] updating [queryMessagesLoading] stream
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
Future<void> loadMore({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'typedef.dart';
/// Widget used to provide information about the chat to the widget tree.
/// This Widget is used to react to life cycle changes and system updates.
/// When the app goes into the background, the websocket connection is kept alive
@@ -13,7 +15,7 @@ import 'package:stream_chat/stream_chat.dart';
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final StreamChatClient client;
///
/// MyApp(this.client);
///
@@ -40,17 +42,27 @@ class StreamChatCore extends StatefulWidget {
Key key,
@required this.client,
@required this.child,
this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1),
}) : assert(client != null),
assert(child != null),
super(key: key);
/// Instance of Stream Chat Client containing information about the current
/// application.
final Client client;
final StreamChatClient client;
/// Widget descendant.
final Widget child;
/// The amount of time that will pass before disconnecting the client in the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
/// is in background. Can be used to display various notifications depending
/// upon the [Event.type]
final EventHandler onBackgroundEventReceived;
@override
StreamChatCoreState createState() => StreamChatCoreState();
@@ -73,7 +85,7 @@ class StreamChatCore extends StatefulWidget {
class StreamChatCoreState extends State<StreamChatCore>
with WidgetsBindingObserver {
/// Initialized client used throughout the application.
Client get client => widget.client;
StreamChatClient get client => widget.client;
Timer _disconnectTimer;
@@ -94,56 +106,28 @@ class StreamChatCoreState extends State<StreamChatCore>
WidgetsBinding.instance.addObserver(this);
}
StreamSubscription _newMessageSubscription;
StreamSubscription _eventSubscription;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (state == AppLifecycleState.paused) {
if (client.showLocalNotification != null) {
_newMessageSubscription = client
.on(EventType.messageNew)
.where((e) => e.user?.id != user.id)
.where((e) => e.message.silent != true)
.where((e) => e.message.shadowed != true)
.listen((event) async {
final channel = client.channel(
event.channelType,
id: event.channelId,
);
client.showLocalNotification(
event.message,
ChannelModel(
id: channel.id,
createdAt: channel.createdAt,
extraData: channel.extraData,
type: channel.type,
memberCount: channel.memberCount,
frozen: channel.frozen,
cid: channel.cid,
deletedAt: channel.deletedAt,
config: channel.config,
createdBy: channel.createdBy,
updatedAt: channel.updatedAt,
lastMessageAt: channel.lastMessageAt,
),
);
});
_disconnectTimer = Timer(client.backgroundKeepAlive, () {
client.disconnect();
});
if (widget.onBackgroundEventReceived != null) {
_eventSubscription =
client.on().listen(widget.onBackgroundEventReceived);
_disconnectTimer = Timer(
widget.backgroundKeepAlive,
client.disconnect,
);
} else {
client.disconnect();
}
} else if (state == AppLifecycleState.resumed) {
_newMessageSubscription?.cancel();
_eventSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
NotificationService.handleIosMessageQueue(client);
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
client.connect();
}
}
@@ -154,6 +138,7 @@ class StreamChatCoreState extends State<StreamChatCore>
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_eventSubscription?.cancel();
_disconnectTimer?.cancel();
super.dispose();
}
@@ -1,6 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
/// A signature for a callback which exposes an error and returns a function.
/// This Callback can be used in cases where an API failure occurs and the widget
/// is unable to render data.
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
/// A Signature for a handler function which will expose a [event].
typedef EventHandler = void Function(Event event);
@@ -10,4 +10,5 @@ export 'src/stream_channel.dart';
export 'src/stream_chat_core.dart';
export 'src/user_list_core.dart';
export 'src/users_bloc.dart';
export 'src/typedef.dart';
export 'package:stream_chat/stream_chat.dart';
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 1.0.0-rc
version: 1.0.0-beta
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -10,8 +10,8 @@ environment:
flutter: ">=1.17.0"
dependencies:
stream_chat: ^0.2.23+3
rxdart: ^0.24.1
stream_chat:
path: ../dart_client
flutter:
sdk: flutter
@@ -20,4 +20,5 @@ dev_dependencies:
flutter_test:
sdk: flutter
fake_async: ^1.1.0
pedantic: ^1.9.2
@@ -1,7 +1,7 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
class MockClient extends Mock implements Client {}
class MockClient extends Mock implements StreamChatClient {}
class MockClientState extends Mock implements ClientState {}
@@ -9,7 +9,7 @@ import 'package:mockito/mockito.dart';
import 'mocks.dart';
class MockShowLocalNotifications extends Mock {
void call(Message m, ChannelModel cm);
void call(Event event);
}
void main() {
@@ -97,14 +97,8 @@ void main() {
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
when(client.showLocalNotification)
.thenReturn(showLocalNotificationMock);
when(client.backgroundKeepAlive).thenReturn(Duration(
seconds: 4,
));
final eventStreamController = StreamController<Event>();
when(client.on(EventType.messageNew))
.thenAnswer((_) => eventStreamController.stream);
when(client.on()).thenAnswer((_) => eventStreamController.stream);
when(client.channel('test', id: 'testid')).thenReturn(channel);
@@ -113,6 +107,8 @@ void main() {
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
@@ -144,13 +140,8 @@ void main() {
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
when(client.showLocalNotification).thenReturn(showLocalNotificationMock);
when(client.backgroundKeepAlive).thenReturn(Duration(
seconds: 4,
));
final eventStreamController = StreamController<Event>();
when(client.on(EventType.messageNew))
.thenAnswer((_) => eventStreamController.stream);
when(client.on()).thenAnswer((_) => eventStreamController.stream);
when(client.channel('test', id: 'testid')).thenReturn(channel);
@@ -159,6 +150,8 @@ void main() {
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
@@ -178,12 +171,9 @@ void main() {
);
eventStreamController.add(event);
await untilCalled(showLocalNotificationMock(any, any));
await untilCalled(showLocalNotificationMock(event));
verify(showLocalNotificationMock(
event.message,
any,
)).called(1);
verify(showLocalNotificationMock(event)).called(1);
},
);
}
@@ -0,0 +1,74 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 78910062997c3a836feee883712c241a5fd22983
channel: stable
project_type: package
@@ -0,0 +1,3 @@
## 1.0.0-beta
* Initial release
+219
View File
@@ -0,0 +1,219 @@
SOURCE CODE LICENSE AGREEMENT
IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR
ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT.
THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE
BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE
LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN
INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN
EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE
OF THE SOFTWARE BY CUSTOMER FOR CUSTOMERS BUSINESS PURPOSES AS DESCRIBED IN
AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO
THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND
CUSTOMER TO THIS AGREEMENT.
STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING
CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A
COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS
AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE
USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU
REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF
STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE
READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE
BOUND BY ALL THE TERMS OF THIS AGREEMENT.
IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT,
STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO
NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND
CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE
SOFTWARE.
1. SOFTWARE. The Stream.io software accompanying this Agreement, may include
Source Code, Executable Object Code, associated media, printed materials and
documentation (collectively, the “Software”). The Software also includes any
updates or upgrades to or new versions of the original Software, if and when
made available to you by Stream.io. “Source Code” means computer programming
code in human readable form that is not suitable for machine execution without
the intervening steps of interpretation or compilation. “Executable Object
Code" means the computer programming code in any other form than Source Code
that is not readily perceivable by humans and suitable for machine execution
without the intervening steps of interpretation or compilation. “Site” means a
Customer location controlled by Customer. “Authorized User” means any employee
or contractor of Customer working at the Site, who has signed a written
confidentiality agreement with Customer or is otherwise bound in writing by
confidentiality and use obligations at least as restrictive as those imposed
under this Agreement.
2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in
consideration for the representations, warranties, and covenants made by
Customer in this Agreement, Stream.io grants to Customer, during the term of
this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable
license to:
a. install and use Software Source Code on password protected computers at a Site,
restricted to Authorized Users;
b. create derivative works, improvements (whether or not patentable), extensions
and other modifications to the Software Source Code (“Modifications”) to build
unique scalable newsfeeds, activity streams, and in-app messaging via Streams
application program interface (“API”);
c. compile the Software Source Code to create Executable Object Code versions of
the Software Source Code and Modifications to build such newsfeeds, activity
streams, and in-app messaging via the API;
d. install, execute and use such Executable Object Code versions solely for
Customers internal business use (including development of websites through
which data generated by Stream services will be streamed (“Apps”));
e. use and distribute such Executable Object Code as part of Customers Apps; and
f. make electronic copies of the Software and Modifications as required for backup
or archival purposes.
3. RESTRICTIONS. Customer is responsible for all activities that occur in
connection with the Software. Customer will not, and will not attempt to: (a)
sublicense or transfer the Software or any Source Code related to the Software
or any of Customers rights under this Agreement, except as otherwise provided
in this Agreement, (b) use the Software Source Code for the benefit of a third
party or to operate a service; (c) allow any third party to access or use the
Software Source Code; (d) sublicense or distribute the Software Source Code or
any Modifications in Source Code or other derivative works based on any part of
the Software Source Code; (e) use the Software in any manner that competes with
Stream.io or its business; or (e) otherwise use the Software in any manner that
exceeds the scope of use permitted in this Agreement. Customer shall use the
Software in compliance with any accompanying documentation any laws applicable
to Customer.
4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or
software components that are open source in conjunction with the Software
Source Code or any Modifications in Source Code or in any way that could
subject the Software to any open source licenses.
5. CONTRACTORS. Under the rights granted to Customer under this Agreement,
Customer may permit its employees, contractors, and agencies of Customer to
become Authorized Users to exercise the rights to the Software granted to
Customer in accordance with this Agreement solely on behalf of Customer to
provide services to Customer; provided that Customer shall be liable for the
acts and omissions of all Authorized Users to the extent any of such acts or
omissions, if performed by Customer, would constitute a breach of, or otherwise
give rise to liability to Customer under, this Agreement. Customer shall not
and shall not permit any Authorized User to use the Software except as
expressly permitted in this Agreement.
6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way
to engage in the development of products or services which could be reasonably
construed to provide a complete or partial functional or commercial alternative
to Stream.ios products or services (a “Competitive Product”). Customer shall
ensure that there is no direct or indirect use of, or sharing of, Software
source code, or other information based upon or derived from the Software to
develop such products or services. Without derogating from the generality of
the foregoing, development of Competitive Products shall include having direct
or indirect access to, supervising, consulting or assisting in the development
of, or producing any specifications, documentation, object code or source code
for, all or part of a Competitive Product.
7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement,
Modifications may only be created and used by Customer as permitted by this
Agreement and Modification Source Code may not be distributed to third parties.
Customer will not assert against Stream.io, its affiliates, or their customers,
direct or indirect, agents and contractors, in any way, any patent rights that
Customer may obtain relating to any Modifications for Stream.io, its
affiliates, or their customers, direct or indirect, agents and contractors
manufacture, use, import, offer for sale or sale of any Stream.io products or
services.
8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant
to Stream.io standard download procedures. The Software is deemed accepted upon
delivery.
9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to
provide any support or consultation concerning the Software.
10. TERM AND TERMINATION. The term of this Agreement begins when the Software is
downloaded or accessed and shall continue until terminated. Either party may
terminate this Agreement upon written notice. This Agreement shall
automatically terminate if Customer is or becomes a competitor of Stream.io or
makes or sells any Competitive Products. Upon termination of this Agreement for
any reason, (a) all rights granted to Customer in this Agreement immediately
cease to exist, (b) Customer must promptly discontinue all use of the Software
and return to Stream.io or destroy all copies of the Software in Customers
possession or control. Any continued use of the Software by Customer or attempt
by Customer to exercise any rights under this Agreement after this Agreement
has terminated shall be considered copyright infringement and subject Customer
to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9
shall survive expiration or termination of this Agreement for any reason.
11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual
property rights and proprietary rights relating thereto or embodied therein,
are the exclusive property of Stream.io and its suppliers. Stream.io and its
suppliers reserve all rights in and to the Software not expressly granted to
Customer in this Agreement, and no other licenses or rights are granted by
implication, estoppel or otherwise.
12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMERS
OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND
WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY
KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT
LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS,
QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS
ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED
THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS
SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO
MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND
DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW.
CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE
EXPRESS WARRANTIES IN THIS AGREEMENT.
13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IOS
TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR
THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE,
SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT,
CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND
WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING
TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON
ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO
THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY.
14. General. Customer may not assign or transfer this Agreement, by operation of
law or otherwise, or any of its rights under this Agreement (including the
license rights granted to Customer) to any third party without Stream.ios
prior written consent, which consent will not be unreasonably withheld or
delayed. Stream.io may assign this Agreement, without consent, including, but
limited to, affiliate or any successor to all or substantially all its business
or assets to which this Agreement relates, whether by merger, sale of assets,
sale of stock, reorganization or otherwise. Any attempted assignment or
transfer in violation of the foregoing will be null and void. Stream.io shall
not be liable hereunder by reason of any failure or delay in the performance of
its obligations hereunder for any cause which is beyond the reasonable control.
All notices, consents, and approvals under this Agreement must be delivered in
writing by courier, by electronic mail, or by certified or registered mail,
(postage prepaid and return receipt requested) to the other party at the
address set forth in the customer agreement between Stream.io and Customer and
will be effective upon receipt or when delivery is refused. This Agreement will
be governed by and interpreted in accordance with the laws of the State of
Colorado, without reference to its choice of laws rules. The United Nations
Convention on Contracts for the International Sale of Goods does not apply to
this Agreement. Any action or proceeding arising from or relating to this
Agreement shall be brought in a federal or state court in Denver, Colorado, and
each party irrevocably submits to the jurisdiction and venue of any such court
in any such action or proceeding. All waivers must be in writing. Any waiver or
failure to enforce any provision of this Agreement on one occasion will not be
deemed a waiver of any other provision or of such provision on any other
occasion. If any provision of this Agreement is unenforceable, such provision
will be changed and interpreted to accomplish the objectives of such provision
to the greatest extent possible under applicable law and the remaining
provisions will continue in full force and effect. Customer shall not violate
any applicable law, rule or regulation, including those regarding the export of
technical data. The headings of Sections of this Agreement are for convenience
and are not to be used in interpreting this Agreement. As used in this
Agreement, the word “including” means “including but not limited to.” This
Agreement (including all exhibits and attachments) constitutes the entire
agreement between the parties regarding the subject hereof and supersedes all
prior or contemporaneous agreements, understandings and communication, whether
written or oral. This Agreement may be amended only by a written document
signed by both parties. The terms of any purchase order or similar document
submitted by Customer to Stream.io will have no effect.
@@ -0,0 +1,71 @@
# Official Chat Persistence Client for [Stream Chat](https://getstream.io/chat/)
<p align="center">
<a href="https://getstream.io/chat/flutter/tutorial/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
</p>
> The official Chat Persistence Client for Stream Chat, a service for
> building chat applications.
[![Pub](https://img.shields.io/pub/v/stream_chat_persistence.svg)](https://pub.dartlang.org/packages/stream_chat_persistence)
![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square)
[![Gitter](https://badges.gitter.im/GetStream/stream_chat_persistence.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
![CI](https://github.com/GetStream/stream-chat-flutter/workflows/CI/badge.svg?branch=master)
<img align="right" src="https://getstream.imgix.net/images/ios-chat-tutorial/iphone_chat_art@1x.png?auto=format,enhance" width="50%" />
This package provides a persistence client for fetching and saving chat data locally.
Stream Chat Persistence uses [Moor](https://github.com/simolus3/moor) as a disk cache.
## Add dependency
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_persistence.svg)](https://pub.dartlang.org/packages/stream_chat_persistence)
```yaml
dependencies:
stream_chat_persistence: ^latest_version
```
You should then run `flutter packages get`
## Usage
The usage is pretty simple.
1. Create a new instance of StreamChatPersistenceClient providing `logLevel` and `connectionMode`.
```dart
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
connectionMode: ConnectionMode.background,
);
```
2. Pass the instance to the official Stream chat client.
```dart
final client = StreamChatClient(
apiKey ?? kDefaultStreamApiKey,
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
```
And you are ready to go...
## Flutter Web
Due to Moor web (for offline storage) you need to include the sql.js library:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script defer src="sql-wasm.js"></script>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body></body>
</html>
```
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.
## Contributing
We welcome code changes that improve this library or fix a problem,
please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github.
We are pleased to merge your code into the official repository.
Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first.
See our license file for more details.
@@ -0,0 +1,9 @@
targets:
$default:
builders:
moor_generator:
options:
generate_connect_constructor: true
data_class_to_companions: false
apply_converters_on_variables: true
generate_values_in_copy_with: true
@@ -0,0 +1,3 @@
export 'list_converter.dart';
export 'map_converter.dart';
export 'message_sending_status_converter.dart';
@@ -0,0 +1,23 @@
import 'dart:convert';
import 'package:moor/moor.dart';
/// Maps a [List] of type [T] into a [String] understood
/// by the sqlite backend.
class ListConverter<T> extends TypeConverter<List<T>, String> {
@override
List<T> mapToDart(fromDb) {
if (fromDb == null) {
return null;
}
return List<T>.from(jsonDecode(fromDb) ?? []);
}
@override
String mapToSql(value) {
if (value == null) {
return null;
}
return jsonEncode(value);
}
}
@@ -0,0 +1,23 @@
import 'dart:convert';
import 'package:moor/moor.dart';
/// Maps a [Map] of type [String], [T] into a [String] understood
/// by the sqlite backend.
class MapConverter<T> extends TypeConverter<Map<String, T>, String> {
@override
Map<String, T> mapToDart(fromDb) {
if (fromDb == null) {
return null;
}
return Map<String, T>.from(jsonDecode(fromDb) ?? {});
}
@override
String mapToSql(value) {
if (value == null) {
return null;
}
return jsonEncode(value);
}
}
@@ -0,0 +1,51 @@
import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.dart';
/// Maps a [MessageSendingStatus] into a [int] understood
/// by the sqlite backend.
class MessageSendingStatusConverter
extends TypeConverter<MessageSendingStatus, int> {
@override
MessageSendingStatus mapToDart(int fromDb) {
switch (fromDb) {
case 0:
return MessageSendingStatus.sending;
case 1:
return MessageSendingStatus.sent;
case 2:
return MessageSendingStatus.failed;
case 3:
return MessageSendingStatus.updating;
case 4:
return MessageSendingStatus.failed_update;
case 5:
return MessageSendingStatus.deleting;
case 6:
return MessageSendingStatus.failed_delete;
default:
return null;
}
}
@override
int mapToSql(MessageSendingStatus value) {
switch (value) {
case MessageSendingStatus.sending:
return 0;
case MessageSendingStatus.sent:
return 1;
case MessageSendingStatus.failed:
return 2;
case MessageSendingStatus.updating:
return 3;
case MessageSendingStatus.failed_update:
return 4;
case MessageSendingStatus.deleting:
return 5;
case MessageSendingStatus.failed_delete:
return 6;
default:
return null;
}
}
}

Some files were not shown because too many files have changed in this diff Show More