Merge pull request #240 from GetStream/llc-independence
LLC Independence
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
name: stream_flutter_workflow
|
name: stream_flutter_workflow
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -70,7 +69,23 @@ jobs:
|
|||||||
find . -maxdepth 12 -name "*.java" -print0 \| xargs -0 java -jar $HOME/google-java-format.jar --replace
|
find . -maxdepth 12 -name "*.java" -print0 \| xargs -0 java -jar $HOME/google-java-format.jar --replace
|
||||||
./.github/workflows/scripts/validate-formatting.sh
|
./.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
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
@@ -85,5 +100,5 @@ jobs:
|
|||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
- name: 'Flutter Test'
|
- name: 'Flutter Test'
|
||||||
run: |
|
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
|
flutter test
|
||||||
+6
-2
@@ -33,8 +33,12 @@ scripts:
|
|||||||
flutter build macos
|
flutter build macos
|
||||||
|
|
||||||
|
|
||||||
test: >
|
test:dart: >
|
||||||
melos exec -c 1 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
|
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
|
flutter test
|
||||||
|
|
||||||
test:web: >
|
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
|
## 0.2.24+2
|
||||||
|
|
||||||
- Fix reconnection bug while using tokenProvider
|
- Fix reconnection bug while using tokenProvider
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ You can sign up for a Stream account at https://getstream.io/chat/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dependencies:
|
dependencies:
|
||||||
stream_chat: ^0.2.0
|
stream_chat: ^1.0.0-beta
|
||||||
```
|
```
|
||||||
|
|
||||||
You should then run `flutter packages get`
|
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.
|
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
|
```dart
|
||||||
final client = Client("stream-chat-api-key");
|
final client = StreamChatClient("stream-chat-api-key");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logging
|
### 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.
|
During development you might want to enable more logging information, you can change the default log level when constructing the client.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final client = Client("stream-chat-api-key", logLevel: Level.INFO);
|
final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Custom Logger
|
#### Custom Logger
|
||||||
@@ -52,32 +52,39 @@ myLogHandlerFunction = (LogRecord record) {
|
|||||||
// do something with the record (ie. send it to Sentry or Fabric)
|
// 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
|
### 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
|
final client = StreamChatClient(
|
||||||
|
apiKey ?? kDefaultStreamApiKey,
|
||||||
Due to Moor web (for offline storage) you need to include the sql.js library:
|
logLevel: Level.INFO,
|
||||||
|
)..chatPersistenceClient = CustomChatPersistentClient();
|
||||||
```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.
|
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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
targets:
|
targets:
|
||||||
$default:
|
$default:
|
||||||
builders:
|
builders:
|
||||||
moor_generator:
|
|
||||||
options:
|
|
||||||
generate_connect_constructor: true
|
|
||||||
json_serializable:
|
json_serializable:
|
||||||
options:
|
options:
|
||||||
explicit_to_json: true
|
explicit_to_json: true
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
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.
|
/// project dashboard.
|
||||||
final client = Client('b67pax5b2wdq');
|
final client = StreamChatClient('b67pax5b2wdq');
|
||||||
|
|
||||||
/// Set the current user. In a production scenario, this should be done using
|
/// Set the current user. In a production scenario, this should be done using
|
||||||
/// a backend to generate a user token using our server SDK.
|
/// a backend to generate a user token using our server SDK.
|
||||||
@@ -48,9 +48,9 @@ class StreamExample extends StatelessWidget {
|
|||||||
@required this.channel,
|
@required this.channel,
|
||||||
}) : super(key: key);
|
}) : 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.
|
/// our application and connection state.
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
/// The channel we'd like to observe and participate.
|
/// The channel we'd like to observe and participate.
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
@@ -242,7 +242,7 @@ class _MessageViewState extends State<MessageView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper extension for quickly retrieving the current user id from a [Client].
|
/// Helper extension for quickly retrieving the current user id from a [StreamChatClient].
|
||||||
extension on Client {
|
extension on StreamChatClient {
|
||||||
String get uid => state.user.id;
|
String get uid => state.user.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ class Channel {
|
|||||||
state?.channelStateStream?.map((cs) => cs.channel?.extraData);
|
state?.channelStateStream?.map((cs) => cs.channel?.extraData);
|
||||||
|
|
||||||
/// The main Stream chat client
|
/// The main Stream chat client
|
||||||
Client get client => _client;
|
StreamChatClient get client => _client;
|
||||||
final Client _client;
|
final StreamChatClient _client;
|
||||||
|
|
||||||
String get _channelURL => '/channels/$type/$id';
|
String get _channelURL => '/channels/$type/$id';
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ class Channel {
|
|||||||
user: _client.state.user,
|
user: _client.state.user,
|
||||||
id: messageId,
|
id: messageId,
|
||||||
quotedMessage: quotedMessage,
|
quotedMessage: quotedMessage,
|
||||||
status: MessageSendingStatus.SENDING,
|
status: MessageSendingStatus.sending,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message.parentId != null && message.id == null) {
|
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;
|
return res;
|
||||||
@@ -487,9 +487,9 @@ class Channel {
|
|||||||
PaginationParams options, {
|
PaginationParams options, {
|
||||||
bool preferOffline = false,
|
bool preferOffline = false,
|
||||||
}) async {
|
}) async {
|
||||||
final cachedReplies = await _client.offlineStorage?.getReplies(
|
final cachedReplies = await _client.chatPersistenceClient?.getReplies(
|
||||||
parentId,
|
parentId,
|
||||||
lessThan: options?.lessThan,
|
options: options,
|
||||||
);
|
);
|
||||||
if (cachedReplies != null && cachedReplies.isNotEmpty) {
|
if (cachedReplies != null && cachedReplies.isNotEmpty) {
|
||||||
state?.updateThreadInfo(parentId, cachedReplies);
|
state?.updateThreadInfo(parentId, cachedReplies);
|
||||||
@@ -603,11 +603,10 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (preferOffline && cid != null) {
|
if (preferOffline && cid != null) {
|
||||||
final updatedState = await _client.offlineStorage?.getChannel(
|
final updatedState =
|
||||||
|
await _client.chatPersistenceClient?.getChannelStateByCid(
|
||||||
cid,
|
cid,
|
||||||
limit: messagesPagination?.limit,
|
messagePagination: messagesPagination,
|
||||||
messageLessThan: messagesPagination?.lessThan,
|
|
||||||
messageGreaterThan: messagesPagination?.greaterThan,
|
|
||||||
);
|
);
|
||||||
if (updatedState != null && updatedState.messages.isNotEmpty) {
|
if (updatedState != null && updatedState.messages.isNotEmpty) {
|
||||||
if (state == null) {
|
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
|
/// If [clearHistory] is set to true - all messages will be removed for the user
|
||||||
Future<EmptyResponse> hide({bool clearHistory = false}) async {
|
Future<EmptyResponse> hide({bool clearHistory = false}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
@@ -732,7 +731,7 @@ class Channel {
|
|||||||
|
|
||||||
if (clearHistory == true) {
|
if (clearHistory == true) {
|
||||||
state.truncate();
|
state.truncate();
|
||||||
await _client.offlineStorage?.deleteChannelsMessages([_cid]);
|
await _client.chatPersistenceClient?.deleteMessageByCid(_cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _client.decode(response.data, EmptyResponse.fromJson);
|
return _client.decode(response.data, EmptyResponse.fromJson);
|
||||||
@@ -871,7 +870,7 @@ class ChannelClientState {
|
|||||||
|
|
||||||
_computeInitialUnread();
|
_computeInitialUnread();
|
||||||
|
|
||||||
_channel._client.offlineStorage
|
_channel._client.chatPersistenceClient
|
||||||
?.getChannelThreads(_channel.cid)
|
?.getChannelThreads(_channel.cid)
|
||||||
?.then((threads) {
|
?.then((threads) {
|
||||||
_threads = threads;
|
_threads = threads;
|
||||||
@@ -949,8 +948,8 @@ class ChannelClientState {
|
|||||||
.on(EventType.channelTruncated, EventType.notificationChannelTruncated)
|
.on(EventType.channelTruncated, EventType.notificationChannelTruncated)
|
||||||
.listen((event) async {
|
.listen((event) async {
|
||||||
final channel = event.channel;
|
final channel = event.channel;
|
||||||
await _channel._client.offlineStorage
|
await _channel._client.chatPersistenceClient
|
||||||
?.deleteChannelsMessages([channel.cid]);
|
?.deleteMessageByCid(channel.cid);
|
||||||
truncate();
|
truncate();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -978,7 +977,7 @@ class ChannelClientState {
|
|||||||
<Message>[...messages, ...threads.values.expand((v) => v)]
|
<Message>[...messages, ...threads.values.expand((v) => v)]
|
||||||
.where((message) =>
|
.where((message) =>
|
||||||
message.status != null &&
|
message.status != null &&
|
||||||
message.status != MessageSendingStatus.SENT &&
|
message.status != MessageSendingStatus.sent &&
|
||||||
message.createdAt.isBefore(DateTime.now().subtract(Duration(
|
message.createdAt.isBefore(DateTime.now().subtract(Duration(
|
||||||
seconds: 1,
|
seconds: 1,
|
||||||
))))
|
))))
|
||||||
@@ -1404,7 +1403,7 @@ class ChannelClientState {
|
|||||||
|
|
||||||
set _channelState(ChannelState v) {
|
set _channelState(ChannelState v) {
|
||||||
_channelStateController.add(v);
|
_channelStateController.add(v);
|
||||||
_channel._client.offlineStorage?.updateChannelState(v);
|
_channel._client.chatPersistenceClient?.updateChannelState(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The channel threads related to this channel
|
/// The channel threads related to this channel
|
||||||
@@ -1417,9 +1416,9 @@ class ChannelClientState {
|
|||||||
BehaviorSubject.seeded({});
|
BehaviorSubject.seeded({});
|
||||||
|
|
||||||
set _threads(Map<String, List<Message>> v) {
|
set _threads(Map<String, List<Message>> v) {
|
||||||
_channel._client.offlineStorage?.updateMessages(
|
_channel._client.chatPersistenceClient?.updateMessages(
|
||||||
v.values.expand((v) => v).toList(),
|
|
||||||
_channel.cid,
|
_channel.cid,
|
||||||
|
v.values.expand((v) => v).toList(),
|
||||||
);
|
);
|
||||||
_threadsController.add(v);
|
_threadsController.add(v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class _BaseResponse {
|
|||||||
String duration;
|
String duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.resync] api call
|
/// Model response for [StreamChatClient.resync] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class SyncResponse extends _BaseResponse {
|
class SyncResponse extends _BaseResponse {
|
||||||
/// The list of events
|
/// The list of events
|
||||||
@@ -28,7 +28,7 @@ class SyncResponse extends _BaseResponse {
|
|||||||
_$SyncResponseFromJson(json);
|
_$SyncResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.queryChannels] api call
|
/// Model response for [StreamChatClient.queryChannels] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class QueryChannelsResponse extends _BaseResponse {
|
class QueryChannelsResponse extends _BaseResponse {
|
||||||
/// List of channels state returned by the query
|
/// List of channels state returned by the query
|
||||||
@@ -39,7 +39,7 @@ class QueryChannelsResponse extends _BaseResponse {
|
|||||||
_$QueryChannelsResponseFromJson(json);
|
_$QueryChannelsResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.queryChannels] api call
|
/// Model response for [StreamChatClient.queryChannels] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class TranslateMessageResponse extends _BaseResponse {
|
class TranslateMessageResponse extends _BaseResponse {
|
||||||
/// List of channels state returned by the query
|
/// List of channels state returned by the query
|
||||||
@@ -50,7 +50,7 @@ class TranslateMessageResponse extends _BaseResponse {
|
|||||||
_$TranslateMessageResponseFromJson(json);
|
_$TranslateMessageResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.queryChannels] api call
|
/// Model response for [StreamChatClient.queryChannels] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class QueryMembersResponse extends _BaseResponse {
|
class QueryMembersResponse extends _BaseResponse {
|
||||||
/// List of channels state returned by the query
|
/// List of channels state returned by the query
|
||||||
@@ -61,7 +61,7 @@ class QueryMembersResponse extends _BaseResponse {
|
|||||||
_$QueryMembersResponseFromJson(json);
|
_$QueryMembersResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.queryUsers] api call
|
/// Model response for [StreamChatClient.queryUsers] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class QueryUsersResponse extends _BaseResponse {
|
class QueryUsersResponse extends _BaseResponse {
|
||||||
/// List of users returned by the query
|
/// List of users returned by the query
|
||||||
@@ -94,7 +94,7 @@ class QueryRepliesResponse extends _BaseResponse {
|
|||||||
_$QueryRepliesResponseFromJson(json);
|
_$QueryRepliesResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.getDevices] api call
|
/// Model response for [StreamChatClient.getDevices] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class ListDevicesResponse extends _BaseResponse {
|
class ListDevicesResponse extends _BaseResponse {
|
||||||
/// List of user devices
|
/// List of user devices
|
||||||
@@ -141,7 +141,7 @@ class SendReactionResponse extends _BaseResponse {
|
|||||||
_$SendReactionResponseFromJson(json);
|
_$SendReactionResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.setGuestUser] api call
|
/// Model response for [StreamChatClient.setGuestUser] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class SetGuestUserResponse extends _BaseResponse {
|
class SetGuestUserResponse extends _BaseResponse {
|
||||||
/// Guest user access token
|
/// Guest user access token
|
||||||
@@ -155,7 +155,7 @@ class SetGuestUserResponse extends _BaseResponse {
|
|||||||
_$SetGuestUserResponseFromJson(json);
|
_$SetGuestUserResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.updateUser] api call
|
/// Model response for [StreamChatClient.updateUser] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class UpdateUsersResponse extends _BaseResponse {
|
class UpdateUsersResponse extends _BaseResponse {
|
||||||
/// Updated users
|
/// Updated users
|
||||||
@@ -166,7 +166,7 @@ class UpdateUsersResponse extends _BaseResponse {
|
|||||||
_$UpdateUsersResponseFromJson(json);
|
_$UpdateUsersResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.updateMessage] api call
|
/// Model response for [StreamChatClient.updateMessage] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class UpdateMessageResponse extends _BaseResponse {
|
class UpdateMessageResponse extends _BaseResponse {
|
||||||
/// Message returned by the api call
|
/// Message returned by the api call
|
||||||
@@ -188,7 +188,7 @@ class SendMessageResponse extends _BaseResponse {
|
|||||||
_$SendMessageResponseFromJson(json);
|
_$SendMessageResponseFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [Client.getMessage] api call
|
/// Model response for [StreamChatClient.getMessage] api call
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable(createToJson: false)
|
||||||
class GetMessageResponse extends _BaseResponse {
|
class GetMessageResponse extends _BaseResponse {
|
||||||
/// Message returned by the api call
|
/// 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)
|
@JsonSerializable(createToJson: false)
|
||||||
class SearchMessagesResponse extends _BaseResponse {
|
class SearchMessagesResponse extends _BaseResponse {
|
||||||
/// List of messages returned by the api call
|
/// 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/client.dart';
|
||||||
import 'package:stream_chat/src/exceptions.dart';
|
import 'package:stream_chat/src/exceptions.dart';
|
||||||
|
|
||||||
@@ -15,17 +15,18 @@ class RetryPolicy {
|
|||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
|
|
||||||
/// This function evaluates if we should retry the failure
|
/// 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;
|
shouldRetry;
|
||||||
|
|
||||||
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
|
/// 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)
|
final Duration Function(
|
||||||
retryTimeout;
|
StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
|
||||||
|
|
||||||
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
|
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
|
||||||
RetryPolicy copyWith({
|
RetryPolicy copyWith({
|
||||||
bool Function(Client client, int attempt, ApiError apiError) shouldRetry,
|
bool Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||||
Duration Function(Client client, int attempt, ApiError apiError)
|
shouldRetry,
|
||||||
|
Duration Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||||
retryTimeout,
|
retryTimeout,
|
||||||
int attempt,
|
int attempt,
|
||||||
}) =>
|
}) =>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:stream_chat/src/api/channel.dart';
|
import 'package:stream_chat/src/api/channel.dart';
|
||||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||||
@@ -113,30 +112,30 @@ class RetryQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _sendFailedEvent(Message message) {
|
void _sendFailedEvent(Message message) {
|
||||||
final newStatus = message.status == MessageSendingStatus.SENDING
|
final newStatus = message.status == MessageSendingStatus.sending
|
||||||
? MessageSendingStatus.FAILED
|
? MessageSendingStatus.failed
|
||||||
: (message.status == MessageSendingStatus.UPDATING
|
: (message.status == MessageSendingStatus.updating
|
||||||
? MessageSendingStatus.FAILED_UPDATE
|
? MessageSendingStatus.failed_update
|
||||||
: MessageSendingStatus.FAILED_DELETE);
|
: MessageSendingStatus.failed_delete);
|
||||||
channel.state.addMessage(message.copyWith(
|
channel.state.addMessage(message.copyWith(
|
||||||
status: newStatus,
|
status: newStatus,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage(Message message) async {
|
Future<void> _sendMessage(Message message) async {
|
||||||
if (message.status == MessageSendingStatus.FAILED_UPDATE ||
|
if (message.status == MessageSendingStatus.failed_update ||
|
||||||
message.status == MessageSendingStatus.UPDATING) {
|
message.status == MessageSendingStatus.updating) {
|
||||||
await channel.client.updateMessage(
|
await channel.client.updateMessage(
|
||||||
message,
|
message,
|
||||||
channel.cid,
|
channel.cid,
|
||||||
);
|
);
|
||||||
} else if (message.status == MessageSendingStatus.FAILED ||
|
} else if (message.status == MessageSendingStatus.failed ||
|
||||||
message.status == MessageSendingStatus.SENDING) {
|
message.status == MessageSendingStatus.sending) {
|
||||||
await channel.sendMessage(
|
await channel.sendMessage(
|
||||||
message,
|
message,
|
||||||
);
|
);
|
||||||
} else if (message.status == MessageSendingStatus.FAILED_DELETE ||
|
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||||
message.status == MessageSendingStatus.DELETING) {
|
message.status == MessageSendingStatus.deleting) {
|
||||||
await channel.client.deleteMessage(
|
await channel.client.deleteMessage(
|
||||||
message,
|
message,
|
||||||
channel.cid,
|
channel.cid,
|
||||||
@@ -152,15 +151,15 @@ class RetryQueue {
|
|||||||
messageList.indexWhere((m) => m.id == event.message.id);
|
messageList.indexWhere((m) => m.id == event.message.id);
|
||||||
if (messageIndex == -1 &&
|
if (messageIndex == -1 &&
|
||||||
[
|
[
|
||||||
MessageSendingStatus.FAILED_UPDATE,
|
MessageSendingStatus.failed_update,
|
||||||
MessageSendingStatus.FAILED,
|
MessageSendingStatus.failed,
|
||||||
MessageSendingStatus.FAILED_DELETE,
|
MessageSendingStatus.failed_delete,
|
||||||
].contains(event.message.status)) {
|
].contains(event.message.status)) {
|
||||||
logger?.info('add message from events');
|
logger?.info('add message from events');
|
||||||
add([event.message]);
|
add([event.message]);
|
||||||
} else if (messageIndex != -1 &&
|
} else if (messageIndex != -1 &&
|
||||||
[
|
[
|
||||||
MessageSendingStatus.SENT,
|
MessageSendingStatus.sent,
|
||||||
null,
|
null,
|
||||||
].contains(event.message.status)) {
|
].contains(event.message.status)) {
|
||||||
_messageQueue.remove(messageList[messageIndex]);
|
_messageQueue.remove(messageList[messageIndex]);
|
||||||
@@ -184,16 +183,16 @@ class RetryQueue {
|
|||||||
|
|
||||||
static DateTime _getMessageDate(Message m1) {
|
static DateTime _getMessageDate(Message m1) {
|
||||||
switch (m1.status) {
|
switch (m1.status) {
|
||||||
case MessageSendingStatus.FAILED_DELETE:
|
case MessageSendingStatus.failed_delete:
|
||||||
case MessageSendingStatus.DELETING:
|
case MessageSendingStatus.deleting:
|
||||||
return m1.deletedAt;
|
return m1.deletedAt;
|
||||||
|
|
||||||
case MessageSendingStatus.FAILED:
|
case MessageSendingStatus.failed:
|
||||||
case MessageSendingStatus.SENDING:
|
case MessageSendingStatus.sending:
|
||||||
return m1.createdAt;
|
return m1.createdAt;
|
||||||
|
|
||||||
case MessageSendingStatus.FAILED_UPDATE:
|
case MessageSendingStatus.failed_update:
|
||||||
case MessageSendingStatus.UPDATING:
|
case MessageSendingStatus.updating:
|
||||||
return m1.updatedAt;
|
return m1.updatedAt;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
import '../models/event.dart';
|
import '../models/event.dart';
|
||||||
@@ -99,9 +99,18 @@ class WebSocket {
|
|||||||
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
|
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
|
||||||
final int reconnectionMonitorTimeout;
|
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
|
/// This notifies of connection status changes
|
||||||
final ValueNotifier<ConnectionStatus> connectionStatus =
|
Stream<ConnectionStatus> get connectionStatusStream =>
|
||||||
ValueNotifier(ConnectionStatus.disconnected);
|
_connectionStatusController.stream;
|
||||||
|
|
||||||
String _path;
|
String _path;
|
||||||
int _retryAttempt = 1;
|
int _retryAttempt = 1;
|
||||||
@@ -128,7 +137,7 @@ class WebSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_connecting = true;
|
_connecting = true;
|
||||||
connectionStatus.value = ConnectionStatus.connecting;
|
_connectionStatus = ConnectionStatus.connecting;
|
||||||
|
|
||||||
logger.info('connecting to $_path');
|
logger.info('connecting to $_path');
|
||||||
|
|
||||||
@@ -175,7 +184,7 @@ class WebSocket {
|
|||||||
_reconnecting = false;
|
_reconnecting = false;
|
||||||
_lastEventAt = DateTime.now();
|
_lastEventAt = DateTime.now();
|
||||||
|
|
||||||
connectionStatus.value = ConnectionStatus.connected;
|
_connectionStatus = ConnectionStatus.connected;
|
||||||
_retryAttempt = 1;
|
_retryAttempt = 1;
|
||||||
|
|
||||||
if (!_connectionCompleter.isCompleted) {
|
if (!_connectionCompleter.isCompleted) {
|
||||||
@@ -199,7 +208,7 @@ class WebSocket {
|
|||||||
_connecting = false;
|
_connecting = false;
|
||||||
|
|
||||||
if (!_reconnecting) {
|
if (!_reconnecting) {
|
||||||
connectionStatus.value = ConnectionStatus.disconnected;
|
_connectionStatus = ConnectionStatus.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_connectionCompleter.isCompleted) {
|
if (!_connectionCompleter.isCompleted) {
|
||||||
@@ -258,7 +267,7 @@ class WebSocket {
|
|||||||
logger.info('reconnect');
|
logger.info('reconnect');
|
||||||
if (!_reconnecting) {
|
if (!_reconnecting) {
|
||||||
_reconnecting = true;
|
_reconnecting = true;
|
||||||
connectionStatus.value = ConnectionStatus.connecting;
|
_connectionStatus = ConnectionStatus.connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
_reconnectTimer();
|
_reconnectTimer();
|
||||||
@@ -300,8 +309,8 @@ class WebSocket {
|
|||||||
_cancelTimers();
|
_cancelTimers();
|
||||||
_reconnecting = false;
|
_reconnecting = false;
|
||||||
_manuallyDisconnected = true;
|
_manuallyDisconnected = true;
|
||||||
connectionStatus.value = ConnectionStatus.disconnected;
|
_connectionStatus = ConnectionStatus.disconnected;
|
||||||
connectionStatus.dispose();
|
await _connectionStatusController.close();
|
||||||
return _channel.sink.close();
|
return _channel.sink.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,22 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:logging/logging.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:rxdart/rxdart.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||||
import 'package:stream_chat/src/event_type.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/src/models/own_user.dart';
|
||||||
import 'package:stream_chat/version.dart';
|
import 'package:stream_chat/version.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:pedantic/pedantic.dart' show unawaited;
|
|
||||||
|
|
||||||
import 'api/channel.dart';
|
import 'api/channel.dart';
|
||||||
import 'api/connection_status.dart';
|
import 'api/connection_status.dart';
|
||||||
import 'api/requests.dart';
|
import 'api/requests.dart';
|
||||||
import 'api/responses.dart';
|
import 'api/responses.dart';
|
||||||
import 'api/websocket.dart';
|
import 'api/websocket.dart';
|
||||||
import 'db/offline_storage.dart';
|
import 'db/chat_persistence_client.dart';
|
||||||
import 'exceptions.dart';
|
import 'exceptions.dart';
|
||||||
import 'models/event.dart';
|
import 'models/event.dart';
|
||||||
import 'models/message.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].
|
/// own backend server. Function requires a single [userId].
|
||||||
typedef TokenProvider = Future<String> Function(String 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.
|
/// Provider used to send push notifications.
|
||||||
enum PushProvider {
|
enum PushProvider {
|
||||||
/// Send notifications using Google's Firebase Cloud Messaging
|
/// Send notifications using Google's Firebase Cloud Messaging
|
||||||
@@ -75,31 +64,27 @@ extension on PushProvider {
|
|||||||
/// websocket connection to Stream Chat servers.
|
/// websocket connection to Stream Chat servers.
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```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.
|
/// Create a client instance with default options.
|
||||||
/// You should only create the client once and re-use it across your application.
|
/// You should only create the client once and re-use it across your application.
|
||||||
Client(
|
StreamChatClient(
|
||||||
this.apiKey, {
|
this.apiKey, {
|
||||||
this.tokenProvider,
|
this.tokenProvider,
|
||||||
this.baseURL = _defaultBaseURL,
|
this.baseURL = _defaultBaseURL,
|
||||||
this.logLevel = Level.WARNING,
|
this.logLevel = Level.WARNING,
|
||||||
this.logHandlerFunction,
|
this.logHandlerFunction,
|
||||||
this.persistenceEnabled = true,
|
|
||||||
Duration connectTimeout = const Duration(seconds: 6),
|
Duration connectTimeout = const Duration(seconds: 6),
|
||||||
Duration receiveTimeout = const Duration(seconds: 6),
|
Duration receiveTimeout = const Duration(seconds: 6),
|
||||||
Dio httpClient,
|
Dio httpClient,
|
||||||
this.showLocalNotification,
|
|
||||||
this.backgroundKeepAlive = const Duration(minutes: 1),
|
|
||||||
RetryPolicy retryPolicy,
|
RetryPolicy retryPolicy,
|
||||||
}) {
|
}) {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
_retryPolicy ??= RetryPolicy(
|
_retryPolicy ??= RetryPolicy(
|
||||||
retryTimeout: (Client client, int attempt, ApiError error) =>
|
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
||||||
Duration(seconds: 1 * attempt),
|
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);
|
state = ClientState(this);
|
||||||
@@ -110,10 +95,11 @@ class Client {
|
|||||||
logger.info('instantiating new client');
|
logger.info('instantiating new client');
|
||||||
}
|
}
|
||||||
|
|
||||||
OfflineStorage _offlineStorage;
|
/// Chat persistence client
|
||||||
|
ChatPersistenceClient chatPersistenceClient;
|
||||||
|
|
||||||
/// If true chat data will persist on disk
|
/// Whether the chat persistence is available or not
|
||||||
final bool persistenceEnabled;
|
bool get persistenceEnabled => chatPersistenceClient != null;
|
||||||
|
|
||||||
RetryPolicy _retryPolicy;
|
RetryPolicy _retryPolicy;
|
||||||
|
|
||||||
@@ -122,25 +108,14 @@ class Client {
|
|||||||
/// The retry policy options getter
|
/// The retry policy options getter
|
||||||
RetryPolicy get retryPolicy => _retryPolicy;
|
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
|
/// This client state
|
||||||
ClientState 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.
|
/// During development you might want to enable more logging information, you can change the default log level when constructing the client.
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// final client = Client("stream-chat-api-key", logLevel: Level.INFO);
|
/// final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
|
||||||
/// ```
|
/// ```
|
||||||
final Level logLevel;
|
final Level logLevel;
|
||||||
|
|
||||||
@@ -159,7 +134,7 @@ class Client {
|
|||||||
/// // do something with the record (ie. send it to Sentry or Fabric)
|
/// // 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;
|
LogHandlerFunction logHandlerFunction;
|
||||||
|
|
||||||
@@ -183,7 +158,8 @@ class Client {
|
|||||||
|
|
||||||
static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com';
|
static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com';
|
||||||
static const _tokenExpiredErrorCode = 40;
|
static const _tokenExpiredErrorCode = 40;
|
||||||
VoidCallback _connectionStatusListener;
|
StreamSubscription<ConnectionStatus> _connectionStatusSubscription;
|
||||||
|
Future<void> Function(ConnectionStatus) _connectionStatusHandler;
|
||||||
|
|
||||||
final BehaviorSubject<Event> _controller = BehaviorSubject<Event>();
|
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
|
/// Listen to this or use the [on] method to filter specific event types
|
||||||
Stream<Event> get stream => _controller.stream;
|
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.
|
/// This notifies the connection status of the websocket connection.
|
||||||
/// Listen to this to get notified when the websocket tries to reconnect.
|
/// Listen to this to get notified when the websocket tries to reconnect.
|
||||||
final ValueNotifier<ConnectionStatus> wsConnectionStatus =
|
Stream<ConnectionStatus> get wsConnectionStatusStream =>
|
||||||
ValueNotifier(ConnectionStatus.disconnected);
|
_wsConnectionStatusController.stream;
|
||||||
|
|
||||||
/// The current user token
|
/// The current user token
|
||||||
String token;
|
String token;
|
||||||
@@ -342,12 +328,12 @@ class Client {
|
|||||||
|
|
||||||
/// Call this function to dispose the client
|
/// Call this function to dispose the client
|
||||||
void dispose() async {
|
void dispose() async {
|
||||||
await _offlineStorage?.disconnect();
|
await chatPersistenceClient?.disconnect();
|
||||||
await _disconnect();
|
await _disconnect();
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
await _controller.close();
|
await _controller.close();
|
||||||
state.channels.values.forEach((c) => c.dispose());
|
|
||||||
state.dispose();
|
state.dispose();
|
||||||
|
await _wsConnectionStatusController.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> get _httpHeaders => {
|
Map<String, String> get _httpHeaders => {
|
||||||
@@ -372,11 +358,6 @@ class Client {
|
|||||||
this.token = token;
|
this.token = token;
|
||||||
_anonymous = false;
|
_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) {
|
return connect().then((event) {
|
||||||
_connectCompleter.complete(event);
|
_connectCompleter.complete(event);
|
||||||
return event;
|
return event;
|
||||||
@@ -424,8 +405,8 @@ class Client {
|
|||||||
|
|
||||||
if (!event.isLocal) {
|
if (!event.isLocal) {
|
||||||
if (_synced && event.createdAt != null) {
|
if (_synced && event.createdAt != null) {
|
||||||
await _offlineStorage?.updateConnectionInfo(event);
|
await chatPersistenceClient?.updateConnectionInfo(event);
|
||||||
await _offlineStorage?.updateLastSyncAt(event.createdAt);
|
await chatPersistenceClient?.updateLastSyncAt(event.createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,21 +425,20 @@ class Client {
|
|||||||
/// Connect the client websocket
|
/// Connect the client websocket
|
||||||
Future<Event> connect() async {
|
Future<Event> connect() async {
|
||||||
logger.info('connecting');
|
logger.info('connecting');
|
||||||
if (wsConnectionStatus.value == ConnectionStatus.connecting) {
|
if (wsConnectionStatus == ConnectionStatus.connecting) {
|
||||||
logger.warning('Already connecting');
|
logger.warning('Already connecting');
|
||||||
throw Exception('Already connecting');
|
throw Exception('Already connecting');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wsConnectionStatus.value == ConnectionStatus.connected) {
|
if (wsConnectionStatus == ConnectionStatus.connected) {
|
||||||
logger.warning('Already connected');
|
logger.warning('Already connected');
|
||||||
throw Exception('Already connected');
|
throw Exception('Already connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
wsConnectionStatus.value = ConnectionStatus.connecting;
|
_wsConnectionStatus = ConnectionStatus.connecting;
|
||||||
|
|
||||||
if (persistenceEnabled && _offlineStorage == null) {
|
if (persistenceEnabled) {
|
||||||
_offlineStorage =
|
await chatPersistenceClient.connect(state.user.id);
|
||||||
await connectDatabase(state.user, _detachedLogger('💽'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ws = WebSocket(
|
_ws = WebSocket(
|
||||||
@@ -478,15 +458,16 @@ class Client {
|
|||||||
logger: _detachedLogger('🔌'),
|
logger: _detachedLogger('🔌'),
|
||||||
);
|
);
|
||||||
|
|
||||||
_connectionStatusListener = () async {
|
_connectionStatusHandler = (ConnectionStatus status) async {
|
||||||
final value = _ws.connectionStatus.value;
|
_wsConnectionStatus = status;
|
||||||
wsConnectionStatus.value = value;
|
handleEvent(
|
||||||
handleEvent(Event(
|
Event(
|
||||||
type: EventType.connectionChanged,
|
type: EventType.connectionChanged,
|
||||||
online: value == ConnectionStatus.connected,
|
online: status == ConnectionStatus.connected,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (value == ConnectionStatus.connected &&
|
if (status == ConnectionStatus.connected &&
|
||||||
state.channels?.isNotEmpty == true) {
|
state.channels?.isNotEmpty == true) {
|
||||||
unawaited(queryChannels(filter: {
|
unawaited(queryChannels(filter: {
|
||||||
'cid': {
|
'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 _ws.connect().then((e) async {
|
||||||
await _offlineStorage?.updateConnectionInfo(e);
|
await chatPersistenceClient?.updateConnectionInfo(e);
|
||||||
event = e;
|
event = e;
|
||||||
await resync();
|
await resync();
|
||||||
}).catchError((err, stacktrace) {
|
}).catchError((err, stacktrace) {
|
||||||
@@ -526,14 +508,14 @@ class Client {
|
|||||||
|
|
||||||
/// Get the events missed while offline to sync the offline storage
|
/// Get the events missed while offline to sync the offline storage
|
||||||
Future<void> resync([List<String> cids]) async {
|
Future<void> resync([List<String> cids]) async {
|
||||||
final lastSyncAt = await offlineStorage?.getLastSyncAt();
|
final lastSyncAt = await chatPersistenceClient?.getLastSyncAt();
|
||||||
|
|
||||||
if (lastSyncAt == null) {
|
if (lastSyncAt == null) {
|
||||||
_synced = true;
|
_synced = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cids ??= await offlineStorage?.getChannelCids();
|
cids ??= await chatPersistenceClient?.getChannelCids();
|
||||||
|
|
||||||
if (cids?.isEmpty == true) {
|
if (cids?.isEmpty == true) {
|
||||||
return;
|
return;
|
||||||
@@ -562,7 +544,7 @@ class Client {
|
|||||||
handleEvent(event);
|
handleEvent(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
await _offlineStorage?.updateLastSyncAt(DateTime.now());
|
await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
|
||||||
_synced = true;
|
_synced = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.severe('Error during resync $error');
|
logger.severe('Error during resync $error');
|
||||||
@@ -590,7 +572,7 @@ class Client {
|
|||||||
logger.info('awaiting connection completer');
|
logger.info('awaiting connection completer');
|
||||||
await _connectCompleter.future;
|
await _connectCompleter.future;
|
||||||
}
|
}
|
||||||
if (wsConnectionStatus.value != ConnectionStatus.connected) {
|
if (wsConnectionStatus != ConnectionStatus.connected) {
|
||||||
final errorMessage =
|
final errorMessage =
|
||||||
'You cannot use queryChannels without an active connection. Please call setUser to connect the client.';
|
'You cannot use queryChannels without an active connection. Please call setUser to connect the client.';
|
||||||
if (persistenceEnabled) {
|
if (persistenceEnabled) {
|
||||||
@@ -708,7 +690,7 @@ class Client {
|
|||||||
channels.add(channel);
|
channels.add(channel);
|
||||||
} else {
|
} else {
|
||||||
final newChannel = Channel.fromState(this, channelState);
|
final newChannel = Channel.fromState(this, channelState);
|
||||||
await _offlineStorage
|
await chatPersistenceClient
|
||||||
?.updateChannelState(newChannel.state.channelState);
|
?.updateChannelState(newChannel.state.channelState);
|
||||||
newChannel.state?.updateChannelState(channelState);
|
newChannel.state?.updateChannelState(channelState);
|
||||||
newChannels[newChannel.cid] = newChannel;
|
newChannels[newChannel.cid] = newChannel;
|
||||||
@@ -719,7 +701,7 @@ class Client {
|
|||||||
|
|
||||||
state.channels = newChannels;
|
state.channels = newChannels;
|
||||||
|
|
||||||
await _offlineStorage?.updateChannelQueries(
|
await chatPersistenceClient?.updateChannelQueries(
|
||||||
filter,
|
filter,
|
||||||
res.channels.map((c) => c.channel.cid).toList(),
|
res.channels.map((c) => c.channel.cid).toList(),
|
||||||
paginationParams?.offset == null || paginationParams.offset == 0,
|
paginationParams?.offset == null || paginationParams.offset == 0,
|
||||||
@@ -754,7 +736,7 @@ class Client {
|
|||||||
@required List<SortOption> sort,
|
@required List<SortOption> sort,
|
||||||
PaginationParams paginationParams = const PaginationParams(limit: 10),
|
PaginationParams paginationParams = const PaginationParams(limit: 10),
|
||||||
}) async {
|
}) async {
|
||||||
final offlineChannels = await _offlineStorage?.getChannelStates(
|
final offlineChannels = await chatPersistenceClient?.getChannelStates(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
sort: sort,
|
sort: sort,
|
||||||
paginationParams: paginationParams,
|
paginationParams: paginationParams,
|
||||||
@@ -769,7 +751,8 @@ class Client {
|
|||||||
return channel;
|
return channel;
|
||||||
} else {
|
} else {
|
||||||
final newChannel = Channel.fromState(this, channelState);
|
final newChannel = Channel.fromState(this, channelState);
|
||||||
_offlineStorage?.updateChannelState(newChannel.state.channelState);
|
chatPersistenceClient
|
||||||
|
?.updateChannelState(newChannel.state.channelState);
|
||||||
newChannels[newChannel.cid] = newChannel;
|
newChannels[newChannel.cid] = newChannel;
|
||||||
return newChannel;
|
return newChannel;
|
||||||
}
|
}
|
||||||
@@ -923,17 +906,17 @@ class Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Closes the websocket connection and resets the 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
|
/// If [clearUser] is true the client unsets the current user
|
||||||
Future<void> disconnect({
|
Future<void> disconnect({
|
||||||
bool flushOfflineStorage = false,
|
bool flushChatPersistence = false,
|
||||||
bool clearUser = false,
|
bool clearUser = false,
|
||||||
}) async {
|
}) async {
|
||||||
logger.info(
|
logger.info(
|
||||||
'Disconnecting flushOfflineStorage: $flushOfflineStorage; clearUser: $clearUser');
|
'Disconnecting flushOfflineStorage: $flushChatPersistence; clearUser: $clearUser');
|
||||||
|
|
||||||
await _offlineStorage?.disconnect(flush: flushOfflineStorage);
|
await chatPersistenceClient?.disconnect(flush: flushChatPersistence);
|
||||||
_offlineStorage = null;
|
chatPersistenceClient = null;
|
||||||
|
|
||||||
if (clearUser == true) {
|
if (clearUser == true) {
|
||||||
state.dispose();
|
state.dispose();
|
||||||
@@ -947,6 +930,7 @@ class Client {
|
|||||||
logger.info('Client disconnecting');
|
logger.info('Client disconnecting');
|
||||||
|
|
||||||
await _ws?.disconnect();
|
await _ws?.disconnect();
|
||||||
|
await _connectionStatusSubscription?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests users with a given query.
|
/// Requests users with a given query.
|
||||||
@@ -1196,7 +1180,7 @@ class Client {
|
|||||||
String cid,
|
String cid,
|
||||||
]) async {
|
]) async {
|
||||||
message = message.copyWith(
|
message = message.copyWith(
|
||||||
status: MessageSendingStatus.UPDATING,
|
status: MessageSendingStatus.updating,
|
||||||
updatedAt: message.updatedAt ?? DateTime.now(),
|
updatedAt: message.updatedAt ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1227,10 +1211,10 @@ class Client {
|
|||||||
|
|
||||||
/// Deletes the given message
|
/// Deletes the given message
|
||||||
Future<EmptyResponse> deleteMessage(Message message, [String cid]) async {
|
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(
|
state.channels[cid].state.addMessage(message.copyWith(
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
status: MessageSendingStatus.SENT,
|
status: MessageSendingStatus.sent,
|
||||||
));
|
));
|
||||||
return EmptyResponse();
|
return EmptyResponse();
|
||||||
}
|
}
|
||||||
@@ -1238,7 +1222,7 @@ class Client {
|
|||||||
try {
|
try {
|
||||||
message = message.copyWith(
|
message = message.copyWith(
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
status: MessageSendingStatus.DELETING,
|
status: MessageSendingStatus.deleting,
|
||||||
deletedAt: message.deletedAt ?? DateTime.now(),
|
deletedAt: message.deletedAt ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1250,7 +1234,7 @@ class Client {
|
|||||||
|
|
||||||
if (state?.channels != null) {
|
if (state?.channels != null) {
|
||||||
state.channels[cid]?.state
|
state.channels[cid]?.state
|
||||||
?.addMessage(message.copyWith(status: MessageSendingStatus.SENT));
|
?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
|
||||||
}
|
}
|
||||||
|
|
||||||
return decode(response.data, EmptyResponse.fromJson);
|
return decode(response.data, EmptyResponse.fromJson);
|
||||||
@@ -1322,7 +1306,7 @@ class ClientState {
|
|||||||
|
|
||||||
void _listenChannelHidden() {
|
void _listenChannelHidden() {
|
||||||
_subscriptions.add(_client.on(EventType.channelHidden).listen((event) {
|
_subscriptions.add(_client.on(EventType.channelHidden).listen((event) {
|
||||||
_client._offlineStorage?.deleteChannels([event.cid]);
|
_client.chatPersistenceClient?.deleteChannels([event.cid]);
|
||||||
if (channels != null) {
|
if (channels != null) {
|
||||||
channels = channels..removeWhere((cid, ch) => cid == event.cid);
|
channels = channels..removeWhere((cid, ch) => cid == event.cid);
|
||||||
}
|
}
|
||||||
@@ -1347,14 +1331,14 @@ class ClientState {
|
|||||||
)
|
)
|
||||||
.listen((Event event) async {
|
.listen((Event event) async {
|
||||||
final eventChannel = event.channel;
|
final eventChannel = event.channel;
|
||||||
await _client._offlineStorage?.deleteChannels([eventChannel.cid]);
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
if (channels != null) {
|
if (channels != null) {
|
||||||
channels = channels..remove(eventChannel.cid);
|
channels = channels..remove(eventChannel.cid);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Client _client;
|
final StreamChatClient _client;
|
||||||
|
|
||||||
/// Update user information
|
/// Update user information
|
||||||
set user(OwnUser user) {
|
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 defining the status of a sending message
|
||||||
enum MessageSendingStatus {
|
enum MessageSendingStatus {
|
||||||
/// Message is being sent
|
/// Message is being sent
|
||||||
SENDING,
|
sending,
|
||||||
|
|
||||||
/// Message is being updated
|
/// Message is being updated
|
||||||
UPDATING,
|
updating,
|
||||||
|
|
||||||
/// Message is being deleted
|
/// Message is being deleted
|
||||||
DELETING,
|
deleting,
|
||||||
|
|
||||||
/// Message failed to send
|
/// Message failed to send
|
||||||
FAILED,
|
failed,
|
||||||
|
|
||||||
/// Message failed to updated
|
/// Message failed to updated
|
||||||
FAILED_UPDATE,
|
failed_update,
|
||||||
|
|
||||||
/// Message failed to delete
|
/// Message failed to delete
|
||||||
FAILED_DELETE,
|
failed_delete,
|
||||||
|
|
||||||
/// Message correctly sent
|
/// Message correctly sent
|
||||||
SENT,
|
sent,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class that contains the information about a message
|
/// The class that contains the information about a message
|
||||||
@@ -72,7 +72,7 @@ class Message {
|
|||||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||||
final List<Reaction> ownReactions;
|
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;
|
final String parentId;
|
||||||
|
|
||||||
/// A quoted reply message
|
/// A quoted reply message
|
||||||
@@ -186,7 +186,7 @@ class Message {
|
|||||||
this.user,
|
this.user,
|
||||||
this.extraData,
|
this.extraData,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
this.status = MessageSendingStatus.SENT,
|
this.status = MessageSendingStatus.sent,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from a json
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Reaction {
|
|||||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
/// The score of the reaction (ie. number of reactions sent)
|
/// Reaction custom extraData
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final Map<String, dynamic> extraData;
|
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/dio_error.dart';
|
||||||
export 'package:dio/src/multipart_file.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/channel.dart';
|
||||||
export './src/api/connection_status.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/reaction.dart';
|
||||||
export './src/models/read.dart';
|
export './src/models/read.dart';
|
||||||
export './src/models/user.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';
|
import 'package:stream_chat/src/client.dart';
|
||||||
|
|
||||||
/// Current package version
|
/// 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';
|
const PACKAGE_VERSION = '0.2.24+2';
|
||||||
|
|||||||
@@ -9,28 +9,18 @@ environment:
|
|||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
json_annotation: ^3.0.1
|
json_annotation: ^3.0.1
|
||||||
shared_preferences: ^0.5.7+3
|
|
||||||
logging: ^0.11.4
|
logging: ^0.11.4
|
||||||
dio: ^3.0.10
|
dio: ^3.0.10
|
||||||
web_socket_channel: ^1.1.0
|
web_socket_channel: ^1.1.0
|
||||||
uuid: ^2.2.2
|
uuid: ^2.2.2
|
||||||
async: ^2.4.1
|
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
|
rxdart: ^0.24.1
|
||||||
collection: ^1.14.12
|
collection: ^1.14.12
|
||||||
sqlite3_flutter_libs: ^0.3.0
|
|
||||||
pedantic: ^1.9.2
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^1.10.0
|
build_runner: ^1.10.0
|
||||||
json_serializable: ^3.3.0
|
json_serializable: ^3.3.0
|
||||||
moor_generator: ^3.1.0
|
test: ^1.15.7
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
mockito: ^4.1.1
|
mockito: ^4.1.1
|
||||||
|
pedantic: ^1.9.2
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/native_imp.dart';
|
import 'package:dio/native_imp.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:stream_chat/src/api/requests.dart';
|
import 'package:stream_chat/src/api/requests.dart';
|
||||||
import 'package:stream_chat/src/client.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/event.dart';
|
||||||
import 'package:stream_chat/src/models/message.dart';
|
import 'package:stream_chat/src/models/message.dart';
|
||||||
import 'package:stream_chat/src/models/reaction.dart';
|
import 'package:stream_chat/src/models/reaction.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
class MockDio extends Mock implements DioForNative {}
|
class MockDio extends Mock implements DioForNative {}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -51,7 +51,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -83,7 +83,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -108,7 +108,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -149,7 +149,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -173,7 +173,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -198,7 +198,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -223,7 +223,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -247,7 +247,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -272,7 +272,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -306,7 +306,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -340,7 +340,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -375,7 +375,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -414,7 +414,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -448,7 +448,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -475,7 +475,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -501,7 +501,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -527,7 +527,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -845,7 +845,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1159,7 +1159,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1474,7 +1474,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1789,7 +1789,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1815,7 +1815,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1842,7 +1842,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1863,7 +1863,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1885,7 +1885,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1910,7 +1910,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1935,7 +1935,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1961,7 +1961,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -1993,7 +1993,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -2024,7 +2024,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
@@ -2064,7 +2064,7 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
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';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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/api/responses.dart';
|
||||||
import 'package:stream_chat/src/models/device.dart';
|
import 'package:stream_chat/src/models/device.dart';
|
||||||
import 'package:stream_chat/src/models/member.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';
|
import 'package:stream_chat/src/api/web_socket_channel_stub.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:stream_chat/src/api/connection_status.dart';
|
import 'package:stream_chat/src/api/connection_status.dart';
|
||||||
@@ -67,7 +67,7 @@ void main() {
|
|||||||
await ws.connect();
|
await ws.connect();
|
||||||
|
|
||||||
verify(connectFunc(computedUrl)).called(1);
|
verify(connectFunc(computedUrl)).called(1);
|
||||||
expect(ws.connectionStatus.value, ConnectionStatus.connected);
|
expect(ws.connectionStatus, ConnectionStatus.connected);
|
||||||
|
|
||||||
await streamController.close();
|
await streamController.close();
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/native_imp.dart';
|
import 'package:dio/native_imp.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:stream_chat/src/api/requests.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/exceptions.dart';
|
||||||
import 'package:stream_chat/src/models/message.dart';
|
import 'package:stream_chat/src/models/message.dart';
|
||||||
import 'package:stream_chat/src/models/user.dart';
|
import 'package:stream_chat/src/models/user.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
class MockDio extends Mock implements DioForNative {}
|
class MockDio extends Mock implements DioForNative {}
|
||||||
|
|
||||||
@@ -41,10 +41,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create the object correctly', () {
|
test('should create the object correctly', () {
|
||||||
final client = Client(
|
final client = StreamChatClient('api-key');
|
||||||
'api-key',
|
|
||||||
persistenceEnabled: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(client.baseURL, 'chat-us-east-1.stream-io-api.com');
|
expect(client.baseURL, 'chat-us-east-1.stream-io-api.com');
|
||||||
expect(client.apiKey, 'api-key');
|
expect(client.apiKey, 'api-key');
|
||||||
@@ -58,9 +55,8 @@ void main() {
|
|||||||
print(record.message);
|
print(record.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
connectTimeout: Duration(seconds: 10),
|
connectTimeout: Duration(seconds: 10),
|
||||||
receiveTimeout: Duration(seconds: 12),
|
receiveTimeout: Duration(seconds: 12),
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
@@ -82,10 +78,7 @@ void main() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
test('Channel', () {
|
test('Channel', () {
|
||||||
final client = Client(
|
final client = StreamChatClient('test');
|
||||||
'test',
|
|
||||||
persistenceEnabled: false,
|
|
||||||
);
|
|
||||||
final Map<String, dynamic> data = {'test': 1};
|
final Map<String, dynamic> data = {'test': 1};
|
||||||
final channelClient = client.channel('type', id: 'id', extraData: data);
|
final channelClient = client.channel('type', id: 'id', extraData: data);
|
||||||
expect(channelClient.type, 'type');
|
expect(channelClient.type, 'type');
|
||||||
@@ -100,10 +93,9 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
persistenceEnabled: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final queryParams = {
|
final queryParams = {
|
||||||
@@ -132,10 +124,9 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
persistenceEnabled: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final queryFilter = <String, dynamic>{
|
final queryFilter = <String, dynamic>{
|
||||||
@@ -184,9 +175,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -213,9 +203,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -258,9 +247,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -285,9 +273,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -305,9 +292,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -323,10 +309,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('devToken', () {
|
test('devToken', () {
|
||||||
final client = Client(
|
final client = StreamChatClient('api-key');
|
||||||
'api-key',
|
|
||||||
persistenceEnabled: false,
|
|
||||||
);
|
|
||||||
final token = client.devToken('test');
|
final token = client.devToken('test');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -342,9 +325,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -371,9 +353,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -415,9 +396,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -437,9 +417,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -453,9 +432,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -475,9 +453,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -501,9 +478,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -531,9 +507,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -553,9 +528,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -576,9 +550,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -598,9 +571,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -622,9 +594,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -644,9 +615,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -666,9 +636,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -694,9 +663,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -716,9 +684,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -738,9 +705,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -761,9 +727,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -787,9 +752,8 @@ void main() {
|
|||||||
final mockHttpClientAdapter = MockHttpClientAdapter();
|
final mockHttpClientAdapter = MockHttpClientAdapter();
|
||||||
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: dioHttp,
|
httpClient: dioHttp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -807,9 +771,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -831,9 +794,8 @@ void main() {
|
|||||||
final mockHttpClientAdapter = MockHttpClientAdapter();
|
final mockHttpClientAdapter = MockHttpClientAdapter();
|
||||||
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: dioHttp,
|
httpClient: dioHttp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -851,9 +813,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -875,9 +836,8 @@ void main() {
|
|||||||
final mockHttpClientAdapter = MockHttpClientAdapter();
|
final mockHttpClientAdapter = MockHttpClientAdapter();
|
||||||
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: dioHttp,
|
httpClient: dioHttp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -895,9 +855,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -920,9 +879,8 @@ void main() {
|
|||||||
final mockHttpClientAdapter = MockHttpClientAdapter();
|
final mockHttpClientAdapter = MockHttpClientAdapter();
|
||||||
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: dioHttp,
|
httpClient: dioHttp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -940,9 +898,8 @@ void main() {
|
|||||||
when(mockDio.options).thenReturn(BaseOptions());
|
when(mockDio.options).thenReturn(BaseOptions());
|
||||||
when(mockDio.interceptors).thenReturn(Interceptors());
|
when(mockDio.interceptors).thenReturn(Interceptors());
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -966,9 +923,8 @@ void main() {
|
|||||||
final mockHttpClientAdapter = MockHttpClientAdapter();
|
final mockHttpClientAdapter = MockHttpClientAdapter();
|
||||||
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
dioHttp.httpClientAdapter = mockHttpClientAdapter;
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
'api-key',
|
'api-key',
|
||||||
persistenceEnabled: false,
|
|
||||||
httpClient: dioHttp,
|
httpClient: dioHttp,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:stream_chat/src/models/action.dart';
|
import 'package:stream_chat/src/models/action.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:stream_chat/src/models/attachment.dart';
|
|||||||
import 'package:stream_chat/src/models/action.dart';
|
import 'package:stream_chat/src/models/action.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('src/models/attachment', () {
|
group('src/models/attachment', () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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_config.dart';
|
||||||
import 'package:stream_chat/src/models/channel_state.dart';
|
import 'package:stream_chat/src/models/channel_state.dart';
|
||||||
import 'package:stream_chat/src/models/command.dart';
|
import 'package:stream_chat/src/models/command.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:stream_chat/src/models/channel_model.dart';
|
import 'package:stream_chat/src/models/channel_model.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:stream_chat/src/models/command.dart';
|
import 'package:stream_chat/src/models/command.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('src/models/command', () {
|
group('src/models/command', () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:stream_chat/src/models/device.dart';
|
import 'package:stream_chat/src/models/device.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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/event.dart';
|
||||||
import 'package:stream_chat/src/models/own_user.dart';
|
import 'package:stream_chat/src/models/own_user.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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/member.dart';
|
||||||
import 'package:stream_chat/src/models/user.dart';
|
import 'package:stream_chat/src/models/user.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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/attachment.dart';
|
||||||
import 'package:stream_chat/src/models/message.dart';
|
import 'package:stream_chat/src/models/message.dart';
|
||||||
import 'package:stream_chat/src/models/reaction.dart';
|
import 'package:stream_chat/src/models/reaction.dart';
|
||||||
@@ -116,7 +116,7 @@ void main() {
|
|||||||
showInChannel: true,
|
showInChannel: true,
|
||||||
parentId: 'parentId',
|
parentId: 'parentId',
|
||||||
extraData: {'hey': 'test'},
|
extraData: {'hey': 'test'},
|
||||||
status: MessageSendingStatus.SENT,
|
status: MessageSendingStatus.sent,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
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 'package:stream_chat/src/models/reaction.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
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/read.dart';
|
||||||
import 'package:stream_chat/src/models/user.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';
|
import 'package:stream_chat/src/models/serialization.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:stream_chat/src/models/user.dart';
|
import 'package:stream_chat/src/models/user.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:stream_chat/version.dart';
|
import 'package:stream_chat/version.dart';
|
||||||
|
|
||||||
void prepareTest() {
|
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/)
|
# Official Flutter SDK for [Stream Chat](https://getstream.io/chat/)
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
> The official Flutter components for Stream Chat, a service for
|
> 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
|
1. Initialize the `StreamChatTheme` from your existing `MaterialApp` style
|
||||||
```dart
|
```dart
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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
|
2. Construct a custom theme and provide all the customizations needed
|
||||||
```dart
|
```dart
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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/routes/routes.dart';
|
||||||
import 'package:example/stream_version.dart';
|
import 'package:example/stream_version.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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 'choose_user_page.dart';
|
||||||
import 'notifications_service.dart';
|
import 'main.dart';
|
||||||
|
|
||||||
class AdvancedOptionsPage extends StatefulWidget {
|
class AdvancedOptionsPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@@ -272,14 +267,10 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
apiKey,
|
apiKey,
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
showLocalNotification: (!kIsWeb && Platform.isAndroid)
|
)..chatPersistenceClient = chatPersistentClient;
|
||||||
? showLocalNotification
|
|
||||||
: null,
|
|
||||||
persistenceEnabled: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
@@ -289,10 +280,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
|||||||
userToken,
|
userToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!kIsWeb) {
|
|
||||||
initNotifications(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
final secureStorage = FlutterSecureStorage();
|
final secureStorage = FlutterSecureStorage();
|
||||||
secureStorage.write(
|
secureStorage.write(
|
||||||
key: kStreamApiKey,
|
key: kStreamApiKey,
|
||||||
@@ -319,73 +306,13 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
|||||||
await client.disconnect();
|
await client.disconnect();
|
||||||
return;
|
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;
|
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:example/stream_version.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'notifications_service.dart';
|
|
||||||
import 'routes/routes.dart';
|
import 'routes/routes.dart';
|
||||||
|
|
||||||
const kStreamApiKey = 'STREAM_API_KEY';
|
const kStreamApiKey = 'STREAM_API_KEY';
|
||||||
@@ -187,10 +185,6 @@ class ChooseUserPage extends StatelessWidget {
|
|||||||
key: kStreamToken,
|
key: kStreamToken,
|
||||||
value: token,
|
value: token,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!kIsWeb) {
|
|
||||||
initNotifications(client);
|
|
||||||
}
|
|
||||||
Navigator.pushNamedAndRemoveUntil(
|
Navigator.pushNamedAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
Routes.HOME,
|
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
|
/// 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].
|
/// or to retrieve outer scope needed such as messages from the [Channel.state].
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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.
|
/// We also change the message color posted by the current user.
|
||||||
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
|
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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]
|
/// - We retrieve the count of unread messages from [Channel.state]
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -34,7 +34,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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
|
/// 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].
|
/// or to retrieve outer scope needed such as messages from the [Channel.state].
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.client);
|
MyApp(this.client);
|
||||||
|
|
||||||
|
|||||||
@@ -150,116 +150,116 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ValueListenableBuilder<ConnectionStatus>(
|
body: ConnectionStatusBuilder(
|
||||||
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, _) {
|
String statusString = '';
|
||||||
String statusString = '';
|
bool showStatus = true;
|
||||||
bool showStatus = true;
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
statusString = 'Connected';
|
statusString = 'Connected';
|
||||||
showStatus = false;
|
showStatus = false;
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.connecting:
|
case ConnectionStatus.connecting:
|
||||||
statusString = 'Reconnecting...';
|
statusString = 'Reconnecting...';
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
statusString = 'Disconnected';
|
statusString = 'Disconnected';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
showMessage: showStatus,
|
showMessage: showStatus,
|
||||||
tileAnchor: Alignment.topCenter,
|
tileAnchor: Alignment.topCenter,
|
||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient:
|
gradient:
|
||||||
StreamChatTheme.of(context).colorTheme.bgGradient,
|
StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 8,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.symmetric(
|
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
|
||||||
vertical: 8,
|
style: TextStyle(
|
||||||
horizontal: 8,
|
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: GestureDetector(
|
Expanded(
|
||||||
behavior: HitTestBehavior.opaque,
|
child: GestureDetector(
|
||||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
behavior: HitTestBehavior.opaque,
|
||||||
child: ListView.separated(
|
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||||
itemCount: _selectedUsers.length + 1,
|
child: ListView.separated(
|
||||||
separatorBuilder: (_, __) => Container(
|
itemCount: _selectedUsers.length + 1,
|
||||||
height: 1,
|
separatorBuilder: (_, __) => Container(
|
||||||
color: StreamChatTheme.of(context)
|
height: 1,
|
||||||
.colorTheme
|
color: StreamChatTheme.of(context)
|
||||||
.greyWhisper,
|
.colorTheme
|
||||||
),
|
.greyWhisper,
|
||||||
itemBuilder: (_, index) {
|
),
|
||||||
if (index == _selectedUsers.length) {
|
itemBuilder: (_, index) {
|
||||||
return Container(
|
if (index == _selectedUsers.length) {
|
||||||
height: 1,
|
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)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.greyWhisper,
|
.black,
|
||||||
);
|
|
||||||
}
|
|
||||||
final user = _selectedUsers[index];
|
|
||||||
return ListTile(
|
|
||||||
key: ObjectKey(user),
|
|
||||||
leading: UserAvatar(
|
|
||||||
user: user,
|
|
||||||
constraints: BoxConstraints.tightFor(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: Text(
|
padding: const EdgeInsets.all(0),
|
||||||
user.name,
|
splashRadius: 24,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
onPressed: () {
|
||||||
),
|
setState(() {
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
_selectedUsers.remove(user);
|
||||||
horizontal: 12,
|
});
|
||||||
vertical: 8,
|
if (_selectedUsers.isEmpty) {
|
||||||
),
|
Navigator.pop(context, _selectedUsers);
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:example/chat_info_screen.dart';
|
import 'package:example/chat_info_screen.dart';
|
||||||
import 'package:example/choose_user_page.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/services.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
|
||||||
|
|
||||||
import 'notifications_service.dart';
|
import 'notifications_service.dart';
|
||||||
@@ -17,6 +17,11 @@ import 'routes/app_routes.dart';
|
|||||||
import 'routes/routes.dart';
|
import 'routes/routes.dart';
|
||||||
import 'search_text_field.dart';
|
import 'search_text_field.dart';
|
||||||
|
|
||||||
|
final chatPersistentClient = StreamChatPersistenceClient(
|
||||||
|
logLevel: Level.INFO,
|
||||||
|
connectionMode: ConnectionMode.background,
|
||||||
|
);
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
final secureStorage = FlutterSecureStorage();
|
final secureStorage = FlutterSecureStorage();
|
||||||
@@ -24,13 +29,10 @@ void main() async {
|
|||||||
final apiKey = await secureStorage.read(key: kStreamApiKey);
|
final apiKey = await secureStorage.read(key: kStreamApiKey);
|
||||||
final userId = await secureStorage.read(key: kStreamUserId);
|
final userId = await secureStorage.read(key: kStreamUserId);
|
||||||
|
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
apiKey ?? kDefaultStreamApiKey,
|
apiKey ?? kDefaultStreamApiKey,
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
showLocalNotification:
|
)..chatPersistenceClient = chatPersistentClient;
|
||||||
(!kIsWeb && Platform.isAndroid) ? showLocalNotification : null,
|
|
||||||
persistenceEnabled: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
final token = await secureStorage.read(key: kStreamToken);
|
final token = await secureStorage.read(key: kStreamToken);
|
||||||
@@ -38,16 +40,13 @@ void main() async {
|
|||||||
User(id: userId),
|
User(id: userId),
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
if (!kIsWeb) {
|
|
||||||
initNotifications(client);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.client);
|
MyApp(this.client);
|
||||||
|
|
||||||
@@ -68,6 +67,7 @@ class MyApp extends StatelessWidget {
|
|||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return StreamChat(
|
return StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
|
onBackgroundEventReceived: showLocalNotification,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>(
|
builder: (context) => AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
child: child,
|
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.
|
/// 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.
|
/// [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 {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.client);
|
MyApp(this.client);
|
||||||
|
|
||||||
|
|||||||
@@ -134,9 +134,8 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: ValueListenableBuilder<ConnectionStatus>(
|
body: ConnectionStatusBuilder(
|
||||||
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, _) {
|
|
||||||
String statusString = '';
|
String statusString = '';
|
||||||
bool showStatus = true;
|
bool showStatus = true;
|
||||||
|
|
||||||
|
|||||||
@@ -87,227 +87,225 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ValueListenableBuilder<ConnectionStatus>(
|
body: ConnectionStatusBuilder(
|
||||||
valueListenable: StreamChat.of(context).client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, _) {
|
String statusString = '';
|
||||||
String statusString = '';
|
bool showStatus = true;
|
||||||
bool showStatus = true;
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
statusString = 'Connected';
|
statusString = 'Connected';
|
||||||
showStatus = false;
|
showStatus = false;
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.connecting:
|
case ConnectionStatus.connecting:
|
||||||
statusString = 'Reconnecting...';
|
statusString = 'Reconnecting...';
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
statusString = 'Disconnected';
|
statusString = 'Disconnected';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
showMessage: showStatus,
|
showMessage: showStatus,
|
||||||
tileAnchor: Alignment.topCenter,
|
tileAnchor: Alignment.topCenter,
|
||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
floatHeaderSlivers: true,
|
floatHeaderSlivers: true,
|
||||||
headerSliverBuilder:
|
headerSliverBuilder:
|
||||||
(BuildContext context, bool innerBoxIsScrolled) {
|
(BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: SearchTextField(
|
child: SearchTextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (_selectedUsers.isNotEmpty)
|
),
|
||||||
SliverToBoxAdapter(
|
if (_selectedUsers.isNotEmpty)
|
||||||
child: Container(
|
SliverToBoxAdapter(
|
||||||
height: 104,
|
child: Container(
|
||||||
child: ListView.separated(
|
height: 104,
|
||||||
scrollDirection: Axis.horizontal,
|
child: ListView.separated(
|
||||||
itemCount: _selectedUsers.length,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.all(8),
|
itemCount: _selectedUsers.length,
|
||||||
separatorBuilder: (_, __) => SizedBox(width: 16),
|
padding: const EdgeInsets.all(8),
|
||||||
itemBuilder: (_, index) {
|
separatorBuilder: (_, __) => SizedBox(width: 16),
|
||||||
final user = _selectedUsers.elementAt(index);
|
itemBuilder: (_, index) {
|
||||||
return Column(
|
final user = _selectedUsers.elementAt(index);
|
||||||
children: [
|
return Column(
|
||||||
Stack(
|
children: [
|
||||||
children: [
|
Stack(
|
||||||
UserAvatar(
|
children: [
|
||||||
onlineIndicatorAlignment:
|
UserAvatar(
|
||||||
Alignment(0.9, 0.9),
|
onlineIndicatorAlignment:
|
||||||
user: user,
|
Alignment(0.9, 0.9),
|
||||||
showOnlineStatus: true,
|
user: user,
|
||||||
borderRadius: BorderRadius.circular(32),
|
showOnlineStatus: true,
|
||||||
constraints: BoxConstraints.tightFor(
|
borderRadius: BorderRadius.circular(32),
|
||||||
height: 64,
|
constraints: BoxConstraints.tightFor(
|
||||||
width: 64,
|
height: 64,
|
||||||
),
|
width: 64,
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
top: -4,
|
Positioned(
|
||||||
right: -4,
|
top: -4,
|
||||||
child: GestureDetector(
|
right: -4,
|
||||||
onTap: () {
|
child: GestureDetector(
|
||||||
if (_selectedUsers.contains(user)) {
|
onTap: () {
|
||||||
setState(() =>
|
if (_selectedUsers.contains(user)) {
|
||||||
_selectedUsers.remove(user));
|
setState(() =>
|
||||||
}
|
_selectedUsers.remove(user));
|
||||||
},
|
}
|
||||||
child: Container(
|
},
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.white,
|
.whiteSnow,
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.whiteSnow,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: StreamSvgIcon.close(
|
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.black,
|
|
||||||
size: 24,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: StreamSvgIcon.close(
|
||||||
)
|
color: StreamChatTheme.of(context)
|
||||||
],
|
.colorTheme
|
||||||
),
|
.black,
|
||||||
SizedBox(height: 4),
|
size: 24,
|
||||||
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
|
SizedBox(height: 4),
|
||||||
.footnote
|
Text(
|
||||||
.copyWith(
|
user.name.split(' ')[0],
|
||||||
color: StreamChatTheme.of(context)
|
style: TextStyle(
|
||||||
.colorTheme
|
fontWeight: FontWeight.bold,
|
||||||
.grey,
|
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'
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
|
||||||
hide Message;
|
hide Message;
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
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 flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
final initializationSettingsAndroid =
|
final initializationSettingsAndroid =
|
||||||
AndroidInitializationSettings('launch_background');
|
AndroidInitializationSettings('launch_background');
|
||||||
@@ -16,9 +14,9 @@ void showLocalNotification(Message message, ChannelModel channel) async {
|
|||||||
);
|
);
|
||||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||||
await flutterLocalNotificationsPlugin.show(
|
await flutterLocalNotificationsPlugin.show(
|
||||||
message.id.hashCode,
|
event.message.id.hashCode,
|
||||||
'${message.user.name} @ ${channel.name}',
|
event.message.user.name,
|
||||||
message.text,
|
event.message.text,
|
||||||
NotificationDetails(
|
NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: AndroidNotificationDetails(
|
||||||
'message channel',
|
'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) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
final args = settings.arguments;
|
final args = settings.arguments;
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
|
case Routes.APP:
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: Routes.APP),
|
||||||
|
builder: (_) {
|
||||||
|
return MyApp(args);
|
||||||
|
});
|
||||||
case Routes.HOME:
|
case Routes.HOME:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: Routes.HOME),
|
settings: const RouteSettings(name: Routes.HOME),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/// Define all the route names here
|
/// Define all the route names here
|
||||||
class Routes {
|
class Routes {
|
||||||
|
static const String APP = '/app';
|
||||||
static const String HOME = '/home';
|
static const String HOME = '/home';
|
||||||
static const String CHOOSE_USER = '/choose_user';
|
static const String CHOOSE_USER = '/choose_user';
|
||||||
static const String ADVANCED_OPTIONS = '/advance_options';
|
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:
|
/// 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
|
/// 1. The Dart API [StreamChatClient] is initialized with your API Key
|
||||||
/// 2. The current user is set by calling [Client.setUser]
|
/// 2. The current user is set by calling [StreamChatClient.setUser]
|
||||||
/// 3. The client is then passed to the top-level [StreamChat] widget
|
/// 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.
|
/// [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:
|
/// 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
|
/// - 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.
|
/// If you now run the simulator you will see a single channel UI.
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -44,7 +44,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
MyApp(this.client, this.channel);
|
MyApp(this.client, this.channel);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -16,7 +16,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.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].
|
/// 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 {
|
void main() async {
|
||||||
final client = Client(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
@@ -24,7 +24,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
MyApp(this.client);
|
MyApp(this.client);
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter:
|
stream_chat_flutter:
|
||||||
path: ../
|
path: ../
|
||||||
|
stream_chat_persistence:
|
||||||
|
path: ../../stream_chat_persistence
|
||||||
flutter_apns: ^1.4.1
|
flutter_apns: ^1.4.1
|
||||||
flutter_local_notifications: ^2.0.2
|
flutter_local_notifications: ^2.0.2
|
||||||
flutter_svg: ^0.19.1
|
flutter_svg: ^0.19.1
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/back_button.dart';
|
||||||
import 'package:stream_chat_flutter/src/channel_info.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/channel_name.dart';
|
||||||
import 'package:stream_chat_flutter/src/info_tile.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/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 './channel_name.dart';
|
||||||
|
import '../stream_chat_flutter.dart';
|
||||||
import 'channel_image.dart';
|
import 'channel_image.dart';
|
||||||
|
import 'connection_status_builder.dart';
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
@@ -17,7 +18,7 @@ import 'channel_image.dart';
|
|||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// class MyApp extends StatelessWidget {
|
/// class MyApp extends StatelessWidget {
|
||||||
/// final Client client;
|
/// final StreamChatClient client;
|
||||||
/// final Channel channel;
|
/// final Channel channel;
|
||||||
///
|
///
|
||||||
/// MyApp(this.client, this.channel);
|
/// MyApp(this.client, this.channel);
|
||||||
@@ -85,11 +86,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
final _client = StreamChat.of(context).client;
|
|
||||||
|
|
||||||
return ValueListenableBuilder<ConnectionStatus>(
|
return ConnectionStatusBuilder(
|
||||||
valueListenable: _client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, _) {
|
|
||||||
String statusString = '';
|
String statusString = '';
|
||||||
bool showStatus = true;
|
bool showStatus = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.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/src/group_image.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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
|
/// ```dart
|
||||||
/// class MyApp extends StatelessWidget {
|
/// class MyApp extends StatelessWidget {
|
||||||
/// final Client client;
|
/// final StreamChatClient client;
|
||||||
/// final Channel channel;
|
/// final Channel channel;
|
||||||
///
|
///
|
||||||
/// MyApp(this.client, this.channel);
|
/// MyApp(this.client, this.channel);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'connection_status_builder.dart';
|
||||||
|
|
||||||
class ChannelInfo extends StatelessWidget {
|
class ChannelInfo extends StatelessWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
@@ -25,9 +27,8 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
stream: channel.state.membersStream,
|
stream: channel.state.membersStream,
|
||||||
initialData: channel.state.members,
|
initialData: channel.state.members,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return ValueListenableBuilder(
|
return ConnectionStatusBuilder(
|
||||||
valueListenable: client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, child) {
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
return _buildConnectedTitleState(context, snapshot.data);
|
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(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -2,25 +2,26 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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/src/stream_neumorphic_button.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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 'info_tile.dart';
|
||||||
import 'stream_chat.dart';
|
import 'stream_chat.dart';
|
||||||
|
|
||||||
typedef _TitleBuilder = Widget Function(
|
typedef _TitleBuilder = Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ConnectionStatus status,
|
ConnectionStatus status,
|
||||||
Client client,
|
StreamChatClient client,
|
||||||
);
|
);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// It shows the current [Client] status.
|
/// It shows the current [StreamChatClient] status.
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// class MyApp extends StatelessWidget {
|
/// class MyApp extends StatelessWidget {
|
||||||
/// final Client client;
|
/// final StreamChatClient client;
|
||||||
///
|
///
|
||||||
/// MyApp(this.client);
|
/// MyApp(this.client);
|
||||||
///
|
///
|
||||||
@@ -41,8 +42,8 @@ typedef _TitleBuilder = Widget Function(
|
|||||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||||
/// However you can also use it as a normal widget.
|
/// However you can also use it as a normal widget.
|
||||||
///
|
///
|
||||||
/// The widget by default uses the inherited [Client] to fetch information about the status.
|
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
|
||||||
/// However you can also pass your own [Client] if you don't have it in the widget tree.
|
/// 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.
|
/// 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.
|
/// Modify it to change the widget appearance.
|
||||||
@@ -58,8 +59,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.preNavigationCallback,
|
this.preNavigationCallback,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Pass this if you don't have a [Client] in your widget tree.
|
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
/// Use this to build your own title as per different [ConnectionStatus]
|
/// Use this to build your own title as per different [ConnectionStatus]
|
||||||
final _TitleBuilder titleBuilder;
|
final _TitleBuilder titleBuilder;
|
||||||
@@ -79,9 +80,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _client = client ?? StreamChat.of(context).client;
|
final _client = client ?? StreamChat.of(context).client;
|
||||||
final user = _client.state.user;
|
final user = _client.state.user;
|
||||||
return ValueListenableBuilder<ConnectionStatus>(
|
return ConnectionStatusBuilder(
|
||||||
valueListenable: _client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, child) {
|
|
||||||
String statusString = '';
|
String statusString = '';
|
||||||
bool showStatus = true;
|
bool showStatus = true;
|
||||||
|
|
||||||
@@ -130,9 +130,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
StreamNeumorphicButton(
|
StreamNeumorphicButton(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: ValueListenableBuilder<ConnectionStatus>(
|
icon: ConnectionStatusBuilder(
|
||||||
valueListenable: _client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, child) {
|
|
||||||
var color;
|
var color;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
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(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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>[
|
children: <Widget>[
|
||||||
if (widget.showReactions &&
|
if (widget.showReactions &&
|
||||||
(widget.message.status ==
|
(widget.message.status ==
|
||||||
MessageSendingStatus.SENT ||
|
MessageSendingStatus.sent ||
|
||||||
widget.message.status == null))
|
widget.message.status == null))
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment(
|
alignment: Alignment(
|
||||||
@@ -174,7 +174,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
showReactionPickerIndicator:
|
showReactionPickerIndicator:
|
||||||
widget.showReactions &&
|
widget.showReactions &&
|
||||||
(widget.message.status ==
|
(widget.message.status ==
|
||||||
MessageSendingStatus.SENT ||
|
MessageSendingStatus.sent ||
|
||||||
widget.message.status == null),
|
widget.message.status == null),
|
||||||
showInChannelIndicator: false,
|
showInChannelIndicator: false,
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
@@ -203,13 +203,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
children: [
|
children: [
|
||||||
if (widget.showReplyMessage &&
|
if (widget.showReplyMessage &&
|
||||||
(widget.message.status ==
|
(widget.message.status ==
|
||||||
MessageSendingStatus.SENT ||
|
MessageSendingStatus.sent ||
|
||||||
widget.message.status == null) &&
|
widget.message.status == null) &&
|
||||||
widget.message.parentId == null)
|
widget.message.parentId == null)
|
||||||
_buildReplyButton(context),
|
_buildReplyButton(context),
|
||||||
if (widget.showThreadReplyMessage &&
|
if (widget.showThreadReplyMessage &&
|
||||||
(widget.message.status ==
|
(widget.message.status ==
|
||||||
MessageSendingStatus.SENT ||
|
MessageSendingStatus.sent ||
|
||||||
widget.message.status == null) &&
|
widget.message.status == null) &&
|
||||||
widget.message.parentId == null)
|
widget.message.parentId == null)
|
||||||
_buildThreadReplyButton(context),
|
_buildThreadReplyButton(context),
|
||||||
@@ -486,7 +486,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
|
|
||||||
Widget _buildDeleteButton(BuildContext context) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
final isDeleteFailed =
|
final isDeleteFailed =
|
||||||
widget.message.status == MessageSendingStatus.FAILED_DELETE;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _showDeleteDialog(),
|
onTap: () => _showDeleteDialog(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -561,7 +561,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
|
|
||||||
Widget _buildResendMessage(BuildContext context) {
|
Widget _buildResendMessage(BuildContext context) {
|
||||||
final isUpdateFailed =
|
final isUpdateFailed =
|
||||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
|
widget.message.status == MessageSendingStatus.failed_update;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|||||||
@@ -2210,7 +2210,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_mentionedUsers.clear();
|
_mentionedUsers.clear();
|
||||||
|
|
||||||
if (widget.editMessage == null ||
|
if (widget.editMessage == null ||
|
||||||
widget.editMessage.status == MessageSendingStatus.FAILED) {
|
widget.editMessage.status == MessageSendingStatus.failed) {
|
||||||
sendingFuture = channel.sendMessage(message);
|
sendingFuture = channel.sendMessage(message);
|
||||||
} else {
|
} else {
|
||||||
sendingFuture = StreamChat.of(context).client.updateMessage(
|
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 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
|
import 'connection_status_builder.dart';
|
||||||
import 'date_divider.dart';
|
import 'date_divider.dart';
|
||||||
import 'swipeable.dart';
|
import 'swipeable.dart';
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
@@ -301,197 +302,193 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_messageListLength = newMessagesListLength;
|
_messageListLength = newMessagesListLength;
|
||||||
final _client = StreamChat.of(context).client;
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<ConnectionStatus>(
|
ConnectionStatusBuilder(
|
||||||
valueListenable: _client.wsConnectionStatus,
|
statusBuilder: (context, status) {
|
||||||
builder: (context, status, _) {
|
String statusString = '';
|
||||||
String statusString = '';
|
bool showStatus = true;
|
||||||
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) {
|
return InfoTile(
|
||||||
case ConnectionStatus.connected:
|
showMessage: widget.showConnectionStateTile ? showStatus : false,
|
||||||
statusString = 'Connected';
|
tileAnchor: Alignment.topCenter,
|
||||||
showStatus = false;
|
childAnchor: Alignment.topCenter,
|
||||||
break;
|
message: statusString,
|
||||||
case ConnectionStatus.connecting:
|
child: LazyLoadScrollView(
|
||||||
statusString = 'Reconnecting...';
|
onStartOfPage: () async {
|
||||||
break;
|
_inBetweenList = false;
|
||||||
case ConnectionStatus.disconnected:
|
if (!_upToDate) {
|
||||||
statusString = 'Disconnected';
|
_topPaginationActive = false;
|
||||||
break;
|
_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(
|
final message = messages[i];
|
||||||
showMessage:
|
final nextMessage = messages[i - 1];
|
||||||
widget.showConnectionStateTile ? showStatus : false,
|
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||||
tileAnchor: Alignment.topCenter,
|
nextMessage.createdAt.toLocal(),
|
||||||
childAnchor: Alignment.topCenter,
|
Units.DAY,
|
||||||
message: statusString,
|
)) {
|
||||||
child: LazyLoadScrollView(
|
final divider = widget.dateDividerBuilder != null
|
||||||
onStartOfPage: () async {
|
? widget.dateDividerBuilder(
|
||||||
_inBetweenList = false;
|
nextMessage.createdAt.toLocal(),
|
||||||
if (!_upToDate) {
|
)
|
||||||
_topPaginationActive = false;
|
: DateDivider(
|
||||||
_bottomPaginationActive = true;
|
dateTime: nextMessage.createdAt.toLocal(),
|
||||||
return _paginateData(
|
);
|
||||||
|
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,
|
streamChannel,
|
||||||
QueryDirection.bottom,
|
QueryDirection.bottom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
final message = messages[i - 1];
|
||||||
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];
|
Widget messageWidget;
|
||||||
final nextMessage = messages[i - 1];
|
|
||||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
if (i == 1) {
|
||||||
nextMessage.createdAt.toLocal(),
|
messageWidget = _buildBottomMessage(
|
||||||
Units.DAY,
|
context,
|
||||||
)) {
|
message,
|
||||||
final divider = widget.dateDividerBuilder != null
|
messages,
|
||||||
? widget.dateDividerBuilder(
|
streamChannel,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
} else if (i == messages.length - 1) {
|
||||||
final isNextUserSame =
|
messageWidget = _buildTopMessage(
|
||||||
message.user.id == nextMessage.user?.id;
|
context,
|
||||||
final isThread = message.replyCount > 0;
|
message,
|
||||||
final isDeleted = message.isDeleted;
|
messages,
|
||||||
if (timeDiff >= 1 ||
|
streamChannel,
|
||||||
!isNextUserSame ||
|
);
|
||||||
isThread ||
|
} else {
|
||||||
isDeleted) {
|
if (widget.messageBuilder != null) {
|
||||||
return SizedBox(height: 8);
|
messageWidget = Builder(
|
||||||
}
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
return SizedBox(height: 2);
|
builder: (context) => widget.messageBuilder(
|
||||||
},
|
context,
|
||||||
itemBuilder: (context, i) {
|
MessageDetails(
|
||||||
if (i == messages.length + 2) {
|
context,
|
||||||
if (widget.parentMessageBuilder != null) {
|
message,
|
||||||
return widget.parentMessageBuilder(
|
messages,
|
||||||
context,
|
i,
|
||||||
widget.parentMessage,
|
),
|
||||||
);
|
messages),
|
||||||
} 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 {
|
} else {
|
||||||
if (widget.messageBuilder != null) {
|
messageWidget = buildMessage(message, messages, i);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return messageWidget;
|
}
|
||||||
},
|
return messageWidget;
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 20.0,
|
top: 20.0,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (showReactions &&
|
if (showReactions &&
|
||||||
(message.status == MessageSendingStatus.SENT ||
|
(message.status == MessageSendingStatus.sent ||
|
||||||
message.status == null))
|
message.status == null))
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment(
|
alignment: Alignment(
|
||||||
@@ -141,7 +141,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
showReactionPickerIndicator: showReactions &&
|
showReactionPickerIndicator: showReactions &&
|
||||||
(message.status ==
|
(message.status ==
|
||||||
MessageSendingStatus.SENT ||
|
MessageSendingStatus.sent ||
|
||||||
message.status == null),
|
message.status == null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -263,13 +263,13 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
|
|
||||||
bool get hasQuotedMessage => widget.message?.quotedMessage != null;
|
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 =>
|
bool get isUpdateFailed =>
|
||||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
|
widget.message.status == MessageSendingStatus.failed_update;
|
||||||
|
|
||||||
bool get isDeleteFailed =>
|
bool get isDeleteFailed =>
|
||||||
widget.message.status == MessageSendingStatus.FAILED_DELETE;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
|
|
||||||
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
|
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
|
|
||||||
void onLongPress(BuildContext context) {
|
void onLongPress(BuildContext context) {
|
||||||
if (widget.message.isEphemeral ||
|
if (widget.message.isEphemeral ||
|
||||||
widget.message.status == MessageSendingStatus.SENDING) {
|
widget.message.status == MessageSendingStatus.sending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,11 +986,11 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
|
|
||||||
void retryMessage(BuildContext context) {
|
void retryMessage(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
if (widget.message.status == MessageSendingStatus.FAILED) {
|
if (widget.message.status == MessageSendingStatus.failed) {
|
||||||
channel.sendMessage(widget.message);
|
channel.sendMessage(widget.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
|
if (widget.message.status == MessageSendingStatus.failed_update) {
|
||||||
StreamChat.of(context).client.updateMessage(
|
StreamChat.of(context).client.updateMessage(
|
||||||
widget.message,
|
widget.message,
|
||||||
channel.cid,
|
channel.cid,
|
||||||
@@ -998,7 +998,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
|
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
||||||
StreamChat.of(context).client.deleteMessage(
|
StreamChat.of(context).client.deleteMessage(
|
||||||
widget.message,
|
widget.message,
|
||||||
channel.cid,
|
channel.cid,
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ class SendingIndicator extends StatelessWidget {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
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(
|
return StreamSvgIcon.check(
|
||||||
size: size,
|
size: size,
|
||||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (message.status == MessageSendingStatus.SENDING ||
|
if (message.status == MessageSendingStatus.sending ||
|
||||||
message.status == MessageSendingStatus.UPDATING) {
|
message.status == MessageSendingStatus.updating) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.access_time,
|
Icons.access_time,
|
||||||
size: size,
|
size: size,
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_app_badger/flutter_app_badger.dart';
|
import 'package:flutter_app_badger/flutter_app_badger.dart';
|
||||||
import 'package:flutter_portal/flutter_portal.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/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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
|
/// Widget used to provide information about the chat to the widget tree
|
||||||
///
|
///
|
||||||
/// class MyApp extends StatelessWidget {
|
/// class MyApp extends StatelessWidget {
|
||||||
/// final Client client;
|
/// final StreamChatClient client;
|
||||||
///
|
///
|
||||||
/// MyApp(this.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.
|
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
|
||||||
class StreamChat extends StatefulWidget {
|
class StreamChat extends StatefulWidget {
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final StreamChatThemeData streamChatThemeData;
|
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({
|
StreamChat({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.client,
|
@required this.client,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
this.streamChatThemeData,
|
this.streamChatThemeData,
|
||||||
|
this.onBackgroundEventReceived,
|
||||||
|
this.backgroundKeepAlive = const Duration(minutes: 1),
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
@@ -63,7 +73,7 @@ class StreamChat extends StatefulWidget {
|
|||||||
|
|
||||||
/// The current state of the StreamChat widget
|
/// The current state of the StreamChat widget
|
||||||
class StreamChatState extends State<StreamChat> {
|
class StreamChatState extends State<StreamChat> {
|
||||||
Client get client => widget.client;
|
StreamChatClient get client => widget.client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -82,8 +92,10 @@ class StreamChatState extends State<StreamChat> {
|
|||||||
scaffoldBackgroundColor: streamTheme.colorTheme.white,
|
scaffoldBackgroundColor: streamTheme.colorTheme.white,
|
||||||
),
|
),
|
||||||
child: StreamChatCore(
|
child: StreamChatCore(
|
||||||
child: widget.child,
|
|
||||||
client: client,
|
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/channel_media_display_screen.dart';
|
||||||
export 'src/info_tile.dart';
|
export 'src/info_tile.dart';
|
||||||
export 'src/stream_chat.dart';
|
export 'src/stream_chat.dart';
|
||||||
|
export 'src/connection_status_builder.dart';
|
||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ flutter:
|
|||||||
- animations/
|
- animations/
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
mockito: ^4.1.3
|
mockito: ^4.1.3
|
||||||
pedantic: ^1.9.2
|
pedantic: ^1.9.2
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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 {}
|
class MockClientState extends Mock implements ClientState {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
## 1.0.0-rc
|
## 1.0.0-beta
|
||||||
|
|
||||||
* First release
|
* First release
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Official Flutter SDK Core for [Stream Chat](https://getstream.io/chat/)
|
# Official Flutter SDK Core for [Stream Chat](https://getstream.io/chat/)
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
> The official Flutter core components for Stream Chat, a service for
|
> 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';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
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.
|
/// project dashboard.
|
||||||
final client = Client('b67pax5b2wdq');
|
final client = StreamChatClient('b67pax5b2wdq');
|
||||||
|
|
||||||
/// Set the current user. In a production scenario, this should be done using
|
/// Set the current user. In a production scenario, this should be done using
|
||||||
/// a backend to generate a user token using our server SDK.
|
/// a backend to generate a user token using our server SDK.
|
||||||
@@ -43,10 +43,10 @@ class StreamExample extends StatelessWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Instance of Stream Client.
|
/// 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
|
/// user for the application. Performing these actions trigger a websocket connection
|
||||||
/// allowing for real-time updates.
|
/// allowing for real-time updates.
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
/// Extensions can be used to add functionality to the SDK. In the examples
|
||||||
/// below, we add two simple extensions to the [Client] and [Channel].
|
/// below, we add two simple extensions to the [StreamChatClient] and [Channel].
|
||||||
extension on Client {
|
extension on StreamChatClient {
|
||||||
/// Fetches the current user id.
|
/// Fetches the current user id.
|
||||||
String get uid => state.user.id;
|
String get uid => state.user.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class LazyLoadScrollView extends StatefulWidget {
|
|||||||
this.onPageScrollStart,
|
this.onPageScrollStart,
|
||||||
this.onPageScrollEnd,
|
this.onPageScrollEnd,
|
||||||
this.onInBetweenOfPage,
|
this.onInBetweenOfPage,
|
||||||
this.isLoading = false,
|
|
||||||
this.scrollOffset = 100,
|
this.scrollOffset = 100,
|
||||||
}) : assert(child != null),
|
}) : assert(child != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -42,9 +41,6 @@ class LazyLoadScrollView extends StatefulWidget {
|
|||||||
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
|
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
|
||||||
final double scrollOffset;
|
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
|
@override
|
||||||
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
|
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
|||||||
Stream<bool> get queryMessagesLoading =>
|
Stream<bool> get queryMessagesLoading =>
|
||||||
_queryMessagesLoadingController.stream;
|
_queryMessagesLoadingController.stream;
|
||||||
|
|
||||||
/// Calls [Client.search] updating [messageResponses] stream
|
/// Calls [StreamChatClient.search] updating [messageResponses] stream
|
||||||
Future<void> search({
|
Future<void> search({
|
||||||
Map<String, dynamic> filter,
|
Map<String, dynamic> filter,
|
||||||
Map<String, dynamic> messageFilter,
|
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({
|
Future<void> loadMore({
|
||||||
Map<String, dynamic> filter,
|
Map<String, dynamic> filter,
|
||||||
Map<String, dynamic> messageFilter,
|
Map<String, dynamic> messageFilter,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
import 'typedef.dart';
|
||||||
|
|
||||||
/// Widget used to provide information about the chat to the widget tree.
|
/// 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.
|
/// 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
|
/// 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
|
/// ```dart
|
||||||
/// class MyApp extends StatelessWidget {
|
/// class MyApp extends StatelessWidget {
|
||||||
/// final Client client;
|
/// final StreamChatClient client;
|
||||||
///
|
///
|
||||||
/// MyApp(this.client);
|
/// MyApp(this.client);
|
||||||
///
|
///
|
||||||
@@ -40,17 +42,27 @@ class StreamChatCore extends StatefulWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.client,
|
@required this.client,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
|
this.onBackgroundEventReceived,
|
||||||
|
this.backgroundKeepAlive = const Duration(minutes: 1),
|
||||||
}) : assert(client != null),
|
}) : assert(client != null),
|
||||||
assert(child != null),
|
assert(child != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Instance of Stream Chat Client containing information about the current
|
/// Instance of Stream Chat Client containing information about the current
|
||||||
/// application.
|
/// application.
|
||||||
final Client client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
/// Widget descendant.
|
/// Widget descendant.
|
||||||
final Widget child;
|
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
|
@override
|
||||||
StreamChatCoreState createState() => StreamChatCoreState();
|
StreamChatCoreState createState() => StreamChatCoreState();
|
||||||
|
|
||||||
@@ -73,7 +85,7 @@ class StreamChatCore extends StatefulWidget {
|
|||||||
class StreamChatCoreState extends State<StreamChatCore>
|
class StreamChatCoreState extends State<StreamChatCore>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
/// Initialized client used throughout the application.
|
/// Initialized client used throughout the application.
|
||||||
Client get client => widget.client;
|
StreamChatClient get client => widget.client;
|
||||||
|
|
||||||
Timer _disconnectTimer;
|
Timer _disconnectTimer;
|
||||||
|
|
||||||
@@ -94,56 +106,28 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription _newMessageSubscription;
|
StreamSubscription _eventSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (client.state?.user != null) {
|
if (client.state?.user != null) {
|
||||||
if (state == AppLifecycleState.paused) {
|
if (state == AppLifecycleState.paused) {
|
||||||
if (client.showLocalNotification != null) {
|
if (widget.onBackgroundEventReceived != null) {
|
||||||
_newMessageSubscription = client
|
_eventSubscription =
|
||||||
.on(EventType.messageNew)
|
client.on().listen(widget.onBackgroundEventReceived);
|
||||||
.where((e) => e.user?.id != user.id)
|
_disconnectTimer = Timer(
|
||||||
.where((e) => e.message.silent != true)
|
widget.backgroundKeepAlive,
|
||||||
.where((e) => e.message.shadowed != true)
|
client.disconnect,
|
||||||
.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();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
} else if (state == AppLifecycleState.resumed) {
|
} else if (state == AppLifecycleState.resumed) {
|
||||||
_newMessageSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
if (_disconnectTimer?.isActive == true) {
|
if (_disconnectTimer?.isActive == true) {
|
||||||
_disconnectTimer.cancel();
|
_disconnectTimer.cancel();
|
||||||
} else {
|
} else {
|
||||||
if (client.wsConnectionStatus.value ==
|
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
|
||||||
ConnectionStatus.disconnected) {
|
|
||||||
NotificationService.handleIosMessageQueue(client);
|
|
||||||
client.connect();
|
client.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +138,7 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_eventSubscription?.cancel();
|
||||||
_disconnectTimer?.cancel();
|
_disconnectTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
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.
|
/// 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
|
/// This Callback can be used in cases where an API failure occurs and the widget
|
||||||
/// is unable to render data.
|
/// is unable to render data.
|
||||||
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
|
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/stream_chat_core.dart';
|
||||||
export 'src/user_list_core.dart';
|
export 'src/user_list_core.dart';
|
||||||
export 'src/users_bloc.dart';
|
export 'src/users_bloc.dart';
|
||||||
|
export 'src/typedef.dart';
|
||||||
export 'package:stream_chat/stream_chat.dart';
|
export 'package:stream_chat/stream_chat.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and 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
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ environment:
|
|||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
stream_chat: ^0.2.23+3
|
stream_chat:
|
||||||
rxdart: ^0.24.1
|
path: ../dart_client
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
@@ -20,4 +20,5 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
fake_async: ^1.1.0
|
fake_async: ^1.1.0
|
||||||
|
pedantic: ^1.9.2
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:stream_chat/stream_chat.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 {}
|
class MockClientState extends Mock implements ClientState {}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:mockito/mockito.dart';
|
|||||||
import 'mocks.dart';
|
import 'mocks.dart';
|
||||||
|
|
||||||
class MockShowLocalNotifications extends Mock {
|
class MockShowLocalNotifications extends Mock {
|
||||||
void call(Message m, ChannelModel cm);
|
void call(Event event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -97,14 +97,8 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
||||||
when(client.showLocalNotification)
|
|
||||||
.thenReturn(showLocalNotificationMock);
|
|
||||||
when(client.backgroundKeepAlive).thenReturn(Duration(
|
|
||||||
seconds: 4,
|
|
||||||
));
|
|
||||||
final eventStreamController = StreamController<Event>();
|
final eventStreamController = StreamController<Event>();
|
||||||
when(client.on(EventType.messageNew))
|
when(client.on()).thenAnswer((_) => eventStreamController.stream);
|
||||||
.thenAnswer((_) => eventStreamController.stream);
|
|
||||||
|
|
||||||
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
||||||
|
|
||||||
@@ -113,6 +107,8 @@ void main() {
|
|||||||
StreamChatCore(
|
StreamChatCore(
|
||||||
key: scKey,
|
key: scKey,
|
||||||
client: client,
|
client: client,
|
||||||
|
onBackgroundEventReceived: showLocalNotificationMock,
|
||||||
|
backgroundKeepAlive: const Duration(seconds: 4),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -144,13 +140,8 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
||||||
when(client.showLocalNotification).thenReturn(showLocalNotificationMock);
|
|
||||||
when(client.backgroundKeepAlive).thenReturn(Duration(
|
|
||||||
seconds: 4,
|
|
||||||
));
|
|
||||||
final eventStreamController = StreamController<Event>();
|
final eventStreamController = StreamController<Event>();
|
||||||
when(client.on(EventType.messageNew))
|
when(client.on()).thenAnswer((_) => eventStreamController.stream);
|
||||||
.thenAnswer((_) => eventStreamController.stream);
|
|
||||||
|
|
||||||
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
||||||
|
|
||||||
@@ -159,6 +150,8 @@ void main() {
|
|||||||
StreamChatCore(
|
StreamChatCore(
|
||||||
key: scKey,
|
key: scKey,
|
||||||
client: client,
|
client: client,
|
||||||
|
onBackgroundEventReceived: showLocalNotificationMock,
|
||||||
|
backgroundKeepAlive: const Duration(seconds: 4),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -178,12 +171,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
eventStreamController.add(event);
|
eventStreamController.add(event);
|
||||||
|
|
||||||
await untilCalled(showLocalNotificationMock(any, any));
|
await untilCalled(showLocalNotificationMock(event));
|
||||||
|
|
||||||
verify(showLocalNotificationMock(
|
verify(showLocalNotificationMock(event)).called(1);
|
||||||
event.message,
|
|
||||||
any,
|
|
||||||
)).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