Merge pull request #240 from GetStream/llc-independence
LLC Independence
This commit is contained in:
@@ -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
@@ -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: >
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
generate_connect_constructor: true
|
||||
json_serializable:
|
||||
options:
|
||||
explicit_to_json: true
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,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
|
||||
@@ -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 CUSTOMER’S 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 Stream’s
|
||||
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
|
||||
Customer’s 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 Customer’s 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 Customer’s 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.io’s 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 Customer’s
|
||||
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 CUSTOMER’S
|
||||
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.IO’S
|
||||
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.io’s
|
||||
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.
|
||||
|
||||
[](https://pub.dartlang.org/packages/stream_chat_persistence)
|
||||

|
||||
[](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||

|
||||
<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 [](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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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
Reference in New Issue
Block a user