Merge pull request #551 from GetStream/release/2.0.0

Release/2.0.0
This commit is contained in:
Salvatore Giordano
2021-07-16 17:33:12 +02:00
committed by GitHub
466 changed files with 156720 additions and 26520 deletions
+20
View File
@@ -0,0 +1,20 @@
name: docusaurus
on:
push:
branches:
- master
- develop
paths:
- docusaurus/**
jobs:
push_docusaurus:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: push
uses: GetStream/push-stream-chat-docusaurus-action@main
with:
target-branch: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }}
env:
DOCUSAURUS_GH_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }}
@@ -1,13 +0,0 @@
#!/usr/bin/env bash
BRANCH=$1
if [ "$BRANCH" == "dev" ]
then
# TODO Flutter dev branch is currently broken so we're unable to test MacOS.
echo "TODO: Skipping macOS testing due to Flutter dev branch issue. Switching branch to stable."
BRANCH=stable
fi
git clone https://github.com/flutter/flutter.git --depth 1 -b $BRANCH _flutter
echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin"
@@ -1,6 +0,0 @@
#!/bin/bash
flutter pub global activate melos
echo "::add-path::$HOME/.pub-cache/bin"
echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin"
echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin"
@@ -3,4 +3,4 @@
# Fast fail the script on failures.
set -e
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$'
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$'
+72 -54
View File
@@ -2,6 +2,7 @@ name: stream_flutter_workflow
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
flutter_version: "2.2.2"
on:
pull_request:
@@ -9,97 +10,114 @@ on:
branches:
- master
- develop
paths-ignore:
- 'docs/**'
jobs:
analyze:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: "Git Checkout"
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools'
- name: Cache Flutter dependencies
uses: actions/cache@v2
with:
path: /opt/hostedtoolcache/flutter
key: ${{ env.flutter_version }}-flutter
- name: "Install Flutter"
uses: subosito/flutter-action@v1
with:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: |
./.github/workflows/scripts/install-tools.sh
flutter pub global activate tuneup
- name: 'Bootstrap Workspace'
flutter pub global activate melos
- name: "Bootstrap Workspace"
run: melos bootstrap
- name: 'Dart Analyze'
- name: "Dart Analyze"
run: |
melos exec -c 3 --ignore="*example*" -- \
tuneup check
- name: 'Pub Check'
melos run analyze
- name: "Pub Check"
if: github.ref == 'refs/heads/master'
run: |
melos exec -c 1 --no-private --ignore="*example*" -- \
pub publish --dry-run
melos run lint:pub
format:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- name: "Git Checkout"
uses: actions/checkout@v2
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'
- name: Cache Flutter dependencies
uses: actions/cache@v2
with:
path: /opt/hostedtoolcache/flutter
key: ${{ env.flutter_version }}-flutter
- name: "Install Flutter"
uses: subosito/flutter-action@v1
with:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: flutter pub global activate melos
- name: "Bootstrap Workspace"
run: melos bootstrap
- name: 'Dart'
- name: "Melos Format"
run: melos run format
- name: "Validate Formatting"
run: |
melos exec -c 1 -- \
flutter format .
./.github/workflows/scripts/validate-formatting.sh
test:
runs-on: ubuntu-latest
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- name: "Git Checkout"
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools'
- name: Cache Flutter dependencies
uses: actions/cache@v2
with:
path: /Users/runner/hostedtoolcache/flutter
key: ${{ env.flutter_version }}-flutter
- name: "Install Flutter"
uses: subosito/flutter-action@v1
with:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: |
./.github/workflows/scripts/install-tools.sh
flutter pub global activate coverage
flutter pub global activate remove_from_coverage
- name: 'Bootstrap Workspace'
flutter pub global activate melos
pub global activate remove_from_coverage
- name: "Bootstrap Workspace"
run: melos bootstrap
- name: 'Dart Test'
run: |
cd packages/stream_chat
flutter pub run test --coverage coverage/
format_coverage --lcov --in=coverage/ --out=coverage/lcov.info --packages=.packages --report-on=lib
- name: 'Flutter Test'
run: |
melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test --coverage
- name: CodeCov
run: |
melos exec -c 3 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
"\$MELOS_ROOT_PATH/.github/workflows/scripts/coverage.sh"
bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
- uses: VeryGoodOpenSource/[email protected]
- name: "Flutter Test"
run: melos run test:all
- name: "Collect Coverage"
run: melos run coverage:ignore-file --no-select
- name: "Upload Coverage"
uses: codecov/codecov-action@v1
with:
token: ${{secrets.CODECOV_TOKEN}}
files: packages/*/coverage/lcov.info
- name: "Stream Chat Coverage Check"
uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat/coverage/lcov.info
min_coverage: 40
- uses: VeryGoodOpenSource/[email protected]
min_coverage: 80
- name: "Stream Chat Persistence Coverage Check"
uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 95
- uses: VeryGoodOpenSource/[email protected]
- name: "Stream Chat Flutter Core Coverage Check"
uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 90
- uses: VeryGoodOpenSource/[email protected]
- name: "Stream Chat Flutter Coverage Check"
uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter/coverage/lcov.info
min_coverage: 16
min_coverage: 67
@@ -1,11 +1,14 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
- packages/*/lib/**/*.g.dart
- packages/*/lib/src/emoji
- packages/*/lib/**/*.freezed.dart
linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
@@ -42,7 +45,6 @@ linter:
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
@@ -138,8 +140,7 @@ linter:
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
- cast_nullable_to_non_nullable
- unnecessary_null_checks
- tighten_type_of_initializing_formals
- null_check_on_nullable_type_parameter
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,4 @@
{
"label": "Introduction",
"position": 1
}
@@ -0,0 +1,55 @@
---
id: choose_package
sidebar_position: 2
title: Choosing The Right Flutter Package
---
### Why the SDK is split into different packages
Different applications need different levels of customization and integration with the Stream Chat SDK.
To do this, the Flutter SDK is split into three different packages which build upon the last and give
varying levels of control to the developer. The higher level packages offer better compatibility out of the
box while the lower level SDKs offer fine grained control. There is also a separate package for persistence
which allows you persist data locally which works with all packages.
### How do I choose?
#### The case for stream_chat_flutter
For the quickest way to integrate Stream Chat with your app, the UI SDK (`stream_chat_flutter`) is the
way to go. `stream_chat_flutter` contains prebuilt components that manage most operations like data
fetching, pagination, sending a message, and more. This ensures you have a nearly out-of-the-box
experience adding chat to your applications. It is also possible to use this in conjunction with
lower level operations of the SDK to get the best of both worlds.
:::note
The package allows customization of components to a large extent making it easy to tweak the theme
to match your app colors and such. If you require any additional feature or customization, feel free
to request this through our support channels.
:::
<b>Summary:</b>
For the quickest and easiest way to add Chat to your app with prebuilt UI components, use stream_chat_flutter
#### The case for stream_chat_flutter_core
If your application involves UI that does not fit in with the stream_chat_flutter components, stream_chat_flutter_core
strips away the UI associated with the components and provides the data fetching and manipulation
capabilities while supplying builders for UI. This allows you to implement your own UI and themes
completely independently while not worrying about writing functions for data and pagination.
<b>Summary:</b>
For implementing your own custom UI while not having to worry about lower level API calls, use stream_chat_flutter_core.
#### The case for stream_chat
The stream_chat package is the Low-level Client (LLC) of Stream Chat in Flutter. This package wraps
the underlying functionality of Stream Chat and allows the most customization in terms of UI, data,
and architecture.
<b>Summary:</b>
For the most control over the SDK and dealing with low level calls to the API, use stream_chat.
@@ -0,0 +1,71 @@
---
slug: /
id: introduction
sidebar_position: 1
title: About The Flutter SDK
---
Exploring The Basics Of Stream Chat
![](../assets/sdk_title.png)
Stream Chat is a service that helps you easily build a full chat experience in your Flutter (and more) apps.
This section of the documentation focuses on our Flutter SDK which helps you easily
ship high quality messaging experiences in apps and programs built with the [Flutter toolkit made
by Google](https://flutter.dev).
The Stream Chat Flutter SDK comprises of four different packages to choose from ranging from ones
giving you complete control to ones that give you a rich out-of-the-box chat experience.
The packages that make up the Stream Chat SDK are:
1. <b>Low Level Client (stream_chat)</b>: a pure Dart package that can be used on any Dart project.
It provides a low-level client to access the Stream Chat service.
2. <b>Core (stream_chat_flutter_core)</b>: provides business logic to fetch common things required
for integrating Stream Chat into your application.
The core package allows more customisation and hence provides business logic but no UI components.
3. <b>UI (stream_chat_flutter)</b>: this library includes both a low-level chat SDK and a set of
reusable and customisable UI components.
4. <b>Persistence (stream_chat_persistence)</b>: provides a persistence client for fetching and
saving chat data locally.
We recommend building prototypes using the full UI package since it contains UI widgets already
integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter)
is the fastest way to get up and running using Stream chat in your app.
The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web
and Desktop.
If you're building a very custom UI and would prefer a more lean package,
our [core package](https://pub.dev/packages/stream_chat_flutter) will be suited to this use case. Core allows you to build custom,
expressive UIs while retaining the benefits of our full Flutter SDK.
APIs for accessing and controlling users, sending messages, etc are seamlessly integrated into
this package and accessible via providers and builders.
Before going into the docs, let's take a small detour to look at how the elements of Stream Chat are structured.
There are two core elements in chat, <b>Users and Channels.</b>
Channels are groups of one or more users that can message each other.
In an app, you need to have a user connected to query channels.
There is no specific distinction between a chat between two people and a group chat,
but there is a way to create a unique chat between a certain number of people by creating a <b>distinct</b> channel.
![](../assets/chat_basics.png)
In essence, a normal two-person chat would be a <b>distinct channel</b> created with two members (you cannot add or delete members in this channel), whereas a group created with two people would simply be a <b>non distinct channel</b> (possible to add or remove members).
Note: It is also possible to add more than two people in a distinct channel which retains the same add/removal properties and resembles the Slack DMs where you can DM one or more people as well.
In summary, if you were creating a Whatsapp-like app, the first screen would be a <b>list of channels</b> - which on opening would show a <b>list of messages</b> that were sent by the users in the Channel.
While this is a simplistic overview of the service, the Flutter SDK handles the UI and more time consuming things (media upload, offline storage, theming, etc.) for you.
Before reading the docs, consider trying our [online API tour](https://getstream.io/chat/get_started/),
it is a nice way to learn how the API works.
It's in-browser so Javascript-based but the ideas are pretty much the same as Dart.
You may also like to look at the [Flutter tutorial](https://getstream.io/chat/flutter/tutorial/)
which focuses on using the UI package to get Stream Chat integrated into a Flutter app.
Further sections break down each individual packages and explain several common operations.
@@ -0,0 +1,4 @@
{
"label": "Guides",
"position": 2
}
@@ -0,0 +1,260 @@
---
id: adding_custom_attachments
sidebar_position: 4
title: Adding Custom Attachments
---
Adding Your Own Types Of Attachments To A Message
### Introduction
Stream Chat supports attachment types like images, video and files by default. You can also add your
own types of attachments through the SDK such as location, audio, etc.
This involves doing three things:
1) Rendering the attachment thumbnail in the `MessageInput`
2) Sending a message with the custom attachment
3) Rendering the custom message attachment
To do this, let's check out an example to add location sharing to Stream Chat.
### Location Sharing
Let's build an example of location sharing option in the app:
![](../assets/location_sharing_example.jpg)
* Show a "Share Location" button next to MessageInput Textfield.
* When the user presses this button, it should fetch the current location coordinates of the user, and send a message on the channel as follows:
```dart
Message(
text: 'This is my location',
attachments: [
Attachment(
uploadState: UploadState.success(),
type: 'location',
extraData: {
'latitude': 'fetched_latitude',
'longitude': 'fetched_longitude',
},
),
],
)
```
For our example, we are going to use [geolocator](https://pub.dev/packages/geolocator) library.
Please check their [setup instructions](https://pub.dev/packages/geolocator) on their docs.
NOTE: If you are testing on iOS simulator, you will need to set some dummy coordinates, as mentioned [here](https://stackoverflow.com/a/31238119/7489541).
Also don't forget to enable "location update" capability in background mode, from XCode.
On the receiver end, `location` type attachment should be rendered in map view, in the `MessageListView`.
We are going to use [Google Static Maps API](https://developers.google.com/maps/documentation/maps-static/overview) to render the map in the message.
You can use other libraries as well such as [google_maps_flutter](https://pub.dev/packages/google_maps_flutter).
First, we add a button which when clicked fetches and shares location into the `MessageInput`:
```dart
MessageInput(
actions: [
InkWell(
child: Icon(
Icons.location_on,
size: 20.0,
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
var channel = StreamChannel.of(context).channel;
var user = StreamChat.of(context).user;
_determinePosition().then((value) {
channel.sendMessage(
Message(
text: 'This is my location',
attachments: [
Attachment(
uploadState: UploadState.success(),
type: 'location',
extraData: {
'latitude': value.latitude.toString(),
'longitude': value.longitude.toString(),
},
),
],
),
);
}).catchError((err) {
print('Error getting location!');
});
},
),
],
),
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
}
if (permission == LocationPermission.denied) {
return Future.error(
'Location permissions are denied');
}
}
return await Geolocator.getCurrentPosition();
}
```
Next, we build the Static Maps URL (Add your API key before using the code snippet):
```dart
String _buildMapAttachment(String lat, String long) {
var baseURL = 'https://maps.googleapis.com/maps/api/staticmap?';
var url = Uri(
scheme: 'https',
host: 'maps.googleapis.com',
port: 443,
path: '/maps/api/staticmap',
queryParameters: {
'center': '${lat},${long}',
'zoom': '15',
'size': '600x300',
'maptype': 'roadmap',
'key': 'YOUR_API_KEY',
'markers': 'color:red|${lat},${long}'
});
return url.toString();
}
```
And then modify the MessageListView and tell it how to build a location attachment:
```dart
MessageListView(
customAttachmentBuilders: {
'location': (context, message, attachments) {
var attachmentWidget = Image.network(
_buildMapAttachment(
attachments[0].extraData['latitude'],
attachments[0].extraData['longitude'],
),
);
return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0));
}
},
),
```
This gives us the final location attachment:
![](../assets/location_sharing_example_message.jpg)
Additionally, you can also add a thumbnail if a message has a location attachment (unlike in this case, where we sent the message directly).
To do this, we will:
1) Add an attachment instead of sending a message
2) Customize the `MessageInput`
First, we add the attachment when the location button is clicked:
```dart
GlobalKey<MessageInputState> _messageInputKey = GlobalKey();
MessageInput(
key: _messageInputKey,
actions: [
InkWell(
child: Icon(
Icons.location_on,
size: 20.0,
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
_determinePosition().then((value) {
_messageInputKey.currentState.addAttachment(
Attachment(
uploadState: UploadState.success(),
type: 'location',
extraData: {
'latitude': value.latitude.toString(),
'longitude': value.longitude.toString(),
},
),
);
}).catchError((err) {
print('Error getting location!');
});
},
),
],
),
```
After this, we can build the thumbnail:
```dart
MessageInput(
key: _messageInputKey,
actions: [
InkWell(
child: Icon(
Icons.location_on,
size: 20.0,
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
_determinePosition().then((value) {
_messageInputKey.currentState.addAttachment(
Attachment(
uploadState: UploadState.success(),
type: 'location',
extraData: {
'latitude': value.latitude.toString(),
'longitude': value.longitude.toString(),
},
),
);
}).catchError((err) {
print('Error getting location!');
});
},
),
],
attachmentThumbnailBuilders: {
'location': (context, attachment) {
return Image.network(
_buildMapAttachment(
attachment.extraData['latitude'],
attachment.extraData['longitude'],
),
);
},
},
),
```
And we can see the thumbnails in the MessageInput:
![](../assets/location_sharing_example_message_thumbnail.jpg)
@@ -0,0 +1,6 @@
---
id: local_data_persistence
sidebar_position: 2
title: Adding Local Data Persistence
---
@@ -0,0 +1,234 @@
---
id: adding_push_notifications
sidebar_position: 3
title: Adding Push Notifications
---
Adding Push Notifications To Your Application
### Introduction
Push notifications are a core part of the experience for a messaging app. Users often need to be notified
of new messages and old notifications sometimes need to be updated silently as well.
This guide details how to add push notifications to your app.
Make sure to check [this section](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) of the docs to read about the push delivery logic.
### Setup FCM
To integrate push notifications in your Flutter app you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging).
Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to know how to set up the plugin for both Android and iOS.
Once that's done FCM should be able to send push notifications to your devices.
### Integration with Stream
#### Step 1
From the [Firebase Console](https://console.firebase.google.com/), select the project your app belongs to.
#### Step 2
Click on the gear icon next to `Project Overview` and navigate to **Project settings**
![](../assets/firebase_project_settings.jpeg)
#### Step 3
Navigate to the `Cloud Messaging` tab
#### Step 4
Under `Project Credentials`, locate the `Server key` and copy it
![](../assets/server_key.png)
#### Step 5
Upload the `Server Key` in your chat dashboard
![](../assets/dashboard_firebase_enable.jpeg)
![](../assets/dashboard_firebase_key.jpeg)
:::note
We are setting up the Android section, but this will work for both Android and iOS if you're using Firebase for both of them!
:::
#### Step 6
Save your push notification settings changes
![](../assets/dashboard_save_changes.jpeg)
**OR**
Upload the `Server Key` via API call using a backend SDK
```js
await client.updateAppSettings({
firebase_config: {
server_key: 'server_key',
notification_template: `{"message":{"notification":{"title":"New messages","body":"You have {{ unread_count }} new message(s) from {{ sender.name }}"},"android":{"ttl":"86400s","notification":{"click_action":"OPEN_ACTIVITY_1"}}}}`,
data_template: `{"sender":"{{ sender.id }}","channel":{"type": "{{ channel.type }}","id":"{{ channel.id }}"},"message":"{{ message.id }}"}`
},
});
```
### Registering a device at Stream Backend
Once you configure Firebase server key and set it up on Stream dashboard a device that is supposed to receive push notifications needs to be registered at Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows:
```dart
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase);
});
```
### Possible issues
We only send push notifications when the user doesn't have any active websocket connection (which is established when you call `client.connectUser`). If you set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) property of the StreamChat widget, when your app goes to background, your device will keep the ws connection alive for 1 minute, and so within this period, you won't receive any push notification.
Make sure to read the [general push docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications go bad 😢
### Testing if Push Notifications are Setup Correctly
If you're not sure if you've set up push notifications correctly (e.g. you don't always receive them, they work unreliably), you can follow these steps to make sure your config is correct and working:
1. Clone our repo for push testing git clone [email protected]:GetStream/chat-push-test.git
2. `cd flutter`
3. In folder run `flutter pub get`
4. Input your api key and secret in `lib/main.dart`
5. Change the bundle identifier/application ID and development team/user so you can run the app in your device (**do not** run on iOS simulator, Android emulator is fine)
6. Add your `google-services.json/GoogleService-Info.plist`
7. Run the app
8. Accept push notification permission (iOS only)
9. Tap on `Device ID` and copy it
10. Send the app to background
11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID
```shell
stream chat:push:test -u <USER-ID>
```
You should get a test push notification
### App in the background but still connected
The [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html) widget lets you define a [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) handler in order to handle events while the app is in the background, but the client is still connected.
This is useful because it lets you keep the connection alive in cases in which the app goes in the background just for some seconds (eg: multitasking, picking pictures from the gallery...)
You can even customize the [backgroundKeepAlive](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/backgroundKeepAlive.html) duration.
In order to show notifications in such a case we suggest using the package [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications); follow the package guide to successfully set up the plugin.
Once that's done you should set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html); here is an example:
```dart
...
StreamChat(
client: client,
onBackgroundEventReceived: (e) {
final currentUserId = client.state.user.id;
if (![
EventType.messageNew,
EventType.notificationMessageNew,
].contains(event.type) ||
event.user.id == currentUserId) {
return;
}
if (event.message == null) return;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final initializationSettingsAndroid =
AndroidInitializationSettings('launch_background');
final initializationSettingsIOS = IOSInitializationSettings();
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin.show(
event.message.id.hashCode,
event.message.user.name,
event.message.text,
NotificationDetails(
android: AndroidNotificationDetails(
'message channel',
'Message channel',
'Channel used for showing messages',
priority: Priority.high,
importance: Importance.high,
),
iOS: IOSNotificationDetails(),
),
);
},
child: ....
);
...
```
As you can see we generate a local notification whenever a message.new or notification.message_new event is received.
:::note
Using `flutter_local_notifications` is a great way to implement notifications while the is in foreground too! You can generate a local notification listening to events using the method `streamChatClient.on()` and react to the events you want.
:::
### Saving notification messages to the offline storage
You may want to save received messages when you receive them via a notification so that later on when you open the app they're already there.
To do this we need to update the push notification data payload at Stream Dashboard and clear the notification one:
```json
{
"message_id": "{{ message.id }}",
"channel_id": "{{ channel.id }}",
"channel_type": "{{ channel.type }}"
}
```
Then we need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, learn [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up.
Then during the call `firebaseMessaging.configure(...)` we need to set the `onBackgroundMessage` parameter using a TOP-LEVEL or STATIC function to handle background messages; here is an example:
```dart
Future<dynamic> myBackgroundMessageHandler(message) async {
if (message.containsKey('data')) {
final data = message['data'];
final messageId = data['message_id'];
final channelId = data['channel_id'];
final channelType = data['channel_type'];
final cid = '$channelType:$channelId';
final client = StreamChatClient(apiKey);
final persistenceClient = StreamChatPersistenceClient();
await persistenceClient.connect(userId);
final message = await client.getMessage(messageId).then((res) => res.message);
await persistenceClient.updateMessages(cid, [message]);
persistenceClient.disconnect();
/// This can be done using the package flutter_local_notifications as we did before 👆
_showLocalNotification();
}
}
```
@@ -0,0 +1,5 @@
---
id: introduction
sidebar_position: 1
title: Introduction
---
@@ -0,0 +1,298 @@
---
id: mig_guide_2_0
sidebar_position: 5
title: Migrating to 2.0 (Null-safety)
---
A Migration Guide For Switching To v2.0 Of The Flutter SDK
### Overview
v2.0 of the Stream Chat Flutter SDK brings along several changes - primarily making the SDK null-safe.
Null safety allows your apps to run faster, with fewer errors, and with less code.
Check [this link](https://flutter.dev/docs/null-safety) for more about Null Safety in Flutter.
This guide is intended to enumerate and better explain the changes in the SDK.
The changes will be listed by package and a concise changelog will follow with more info.
### Changelog of `stream_chat_flutter`
#### 🛑️ Breaking Changes from 1.5.4
* Migrate this package to null safety
* Renamed `ChannelImage` to `ChannelAvatar`
* Updated `StreamChatThemeData.reactionIcons` to accept custom builder
* Renamed `ColorTheme` properties to reflect the purpose of the colors
* `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
* `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
* `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
* `ColorTheme.greyWhisper` -> `ColorTheme.borders`
* `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
* `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
* `ColorTheme.white` -> `ColorTheme.barsBg`
* `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
* `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
* `ColorTheme.accentRed` -> `ColorTheme.accentError`
* `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
* `ChannelListCore` options property is removed in favor of individual properties
* `options.state` -> `bool state`
* `options.watch` -> `bool watch`
* `options.presence` -> `bool presence`
* `UserListView` options property is removed in favor of individual properties
* `options.presence` -> `bool presence`
* Renamed `ImageHeader` to `GalleryHeader`
* Renamed `ImageFooter` to `GalleryFooter`
* `MessageBuilder` and `ParentMessageBuilder` signature is now
```
typedef MessageBuilder = Widget Function(
BuildContext,
MessageDetails,
List<Message>,
MessageWidget defaultMessageWidget,
);
```
The last parameter is the default `MessageWidget`
You can call `.copyWith` to customize just a subset of properties
#### ✅ Added
Added video compress options (frame and quality) to MessageInput
`TypingIndicator` now has a property called `parentId` to show typing indicator specific to threads
#493: add support for `MessageListView` header/footer
`MessageWidget` accepts a `userAvatarBuilder`
Added `pinMessage` ui support
Added `MessageListView.threadSeparatorBuilder` property
Added `MessageInput.onError` property to allow error handling
Added `GalleryHeader`/`GalleryFooter` theme classes
#### 🐞 Fixed
#483: Keyboard covers input text box when editing message
`Modals` are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
#484: messages don't update without a reload
`MessageListView` not rendering if the user is not a member of the channel
Fix `MessageInput` overflow when there are no actions
Minor fixes and improvements
### Migrating to 2.0 for `stream_chat_flutter`
:::note
If you are migrating your full Flutter project to null-safety, first make sure you follow the
instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide).
:::
To migrate to v2.0 for `stream_chat_flutter`, first change the version of the package to the latest
null-safe version.
```yaml
dependencies:
stream_chat_flutter: ^2.0.0
```
Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to
remedy the issues:
1) Replace the offending class names with the revised class names
* `ChannelImage` -> `ChannelAvatar`
* `ImageHeader` -> `GalleryHeader`
* `ImageFooter` -> `GalleryFooter`
2) The new version comes with revised color names since the previous names do not suit light/dark mode
nomenclature. Make sure any old colors used from theme are changed over to the new theme color names:
* `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
* `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
* `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
* `ColorTheme.greyWhisper` -> `ColorTheme.borders`
* `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
* `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
* `ColorTheme.white` -> `ColorTheme.barsBg`
* `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
* `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
* `ColorTheme.accentRed` -> `ColorTheme.accentError`
* `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
3) We decided to make messages easier to customize and now supply the default implementation of the
messages in the builder - so you can now customize a single parameter without having to redo the
entire implementation. Please reform your builders to take into account the new format:
```
typedef MessageBuilder = Widget Function(
BuildContext,
MessageDetails,
List<Message>,
MessageWidget defaultMessageWidget,
);
```
To tweak any of the default properties individually, you can use `defaultMessageWidget.copyWith()`.
### Changelog of `stream_chat_flutter_core`
#### 🛑️ Breaking Changes from 1.5.3
* Migrate this package to null safety
* `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties
* `options.state` -> `bool state`
* `options.watch` -> `bool watch`
* `options.presence` -> `bool presence`
* `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties
* `options.presence` -> `bool presence`
#### ✅ Added
* Monitor connection using `connectivity_plus` package
#### 🐞 Fixed
* Minor fixes
* Performance improvements
### Migrating to 2.0 for `stream_chat_flutter_core`
:::note
If you are migrating your full Flutter project to null-safety, first make sure you follow the
instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide).
:::
To migrate to v2.0 for `stream_chat_flutter_core`, first change the version of the package to the latest
null-safe version.
```yaml
dependencies:
stream_chat_flutter_core: ^2.0.0
```
Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to
remedy the issue:
:::note
The major changes in `stream_chat_flutter_core` consist of changing over from a map full of options
to a more type safe and sound approach by changing over to explicit parameters.
:::
1) Change over Core widget implementations by using the explicit parameters instead of the options map.
Use these explicit parameters in the widget constructor instead of the option keys:
* `options.state` -> `bool state`
* `options.watch` -> `bool watch`
* `options.presence` -> `bool presence`
2) Change over query calls in the BLoCs in the same way (change from options map to explicit parameters
in the constructor)
### Changelog of `stream_chat`
#### 🛑️ Breaking Changes from 1.5.3
* Migrate this package to null safety
* `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor)
* `client.disconnect()` is now divided into two different functions
* `client.closeConnection()` -> for closing user websocket connection.
* `client.disconnectUser()` -> for disconnecting user and resetting client state.
* `client.devToken()` now returns a Token model instead of String.
* `ApiError` is removed in favor of `StreamChatError`
* `StreamChatError` -> parent type for all the stream errors.
* `StreamWebSocketError` -> for user websocket related errors.
* `StreamChatNetworkError` -> for network related errors.
* `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
* `option.state` -> `bool state`
* `option.watch` -> `bool watch`
* `option.presence` -> `bool presence`
* `client.queryUsers()` options param is removed in favor of individual params
* `option.presence` -> `bool presence`
* Added typed filters
#### 🐞 Fixed
* #369: Client does not return without internet connection
* Several minor fixes
* Performance improvements
#### ✅ Added
* New Location enum is introduced for easily changing the client location/baseUrl.
* New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection.
* New `client.partialUpdateMessage` and `channel.partialUpdateMessage` methods
* `connectWebSocket` parameter in connect user calls to use the client in "connection-less" mode.
#### 🔄 Changed
* `baseURL` is now deprecated in favor of using Location to change data location.
### Migrating to 2.0 for `stream_chat`
If you are migrating your full Flutter project to null-safety, first make sure you follow the
instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide).
:::
To migrate to v2.0 for `stream_chat`, first change the version of the package to the latest
null-safe version.
```yaml
dependencies:
stream_chat: ^2.0.0
```
Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to
remedy the issues:
1) Change over the constructor of `connectUserWithProvider()` to the new format which has `tokenProvider` as a required param.
2) We added more nuance to `disconnectUser()` by adding two new methods - one to close the connection
and the other to disconnect the user. This allows more fine-grained control of disconnection.
* `client.closeConnection()` -> for closing user websocket connection.
* `client.disconnectUser()` -> for disconnecting user and resetting client state.
3) We refactored how we handle errors - new error types are now introduced that replace ApiError.
* `StreamChatError` -> parent type for all the stream errors.
* `StreamWebSocketError` -> for user websocket related errors.
* `StreamChatNetworkError` -> for network related errors.
4) We changed over from a map full of options to a more type-safe and sound approach by changing over to explicit parameters.
Use these explicit parameters in the query parameters instead of the option keys:
* `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
* `option.state` -> `bool state`
* `option.watch` -> `bool watch`
* `option.presence` -> `bool presence`
* `client.queryUsers()` options param is removed in favor of individual params
* `option.presence` -> `bool presence`
5) We added type-safe filters to make filtering in the app easier. Change over the filters to the
new implementation.
As an example, in the old app this filter:
```dart
filter: {
'members': {
'\$in': [StreamChat.of(context).user.id],
}
},
```
Would turn into:
```dart
filter: Filter.in_('members', [StreamChat.of(context).user.id])
```
@@ -0,0 +1,4 @@
{
"label": "Stream Chat Flutter",
"position": 3
}
@@ -0,0 +1,84 @@
---
id: channel_header
sidebar_position: 10
title: ChannelHeader
---
A Widget To Display Common Channel Details
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelHeader-class.html)
![](../assets/channel_header.png)
### Background
When a user opens a channel, it is helpful to provide context of which channel they are in. This may
be in the form of a channel name or the users in the channel. Along with that, there also needs to be
a way for the user to look at more details of the channel (media, pinned messages, actions, etc.) and
preferably also a way to navigate back to where they came from.
To encapsulate all of this functionality into one widget, the Flutter SDK contains a `ChannelHeader`
widget which provides these out of the box.
### Basic Example
Let's just add a `ChannelHeader` to a page with a `MessageListView` and a `MessageInput` to display
and send messages.
```dart
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
),
),
MessageInput(),
],
),
);
}
}
```
### Customizing Parts Of The Header
The header works like a `ListTile` widget.
Use the `title`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own.
```dart
//...
ChannelHeader(
title: Text('My Custom Name'),
),
```
![](../assets/channel_header_custom_title.png)
### Showing Connection State
The `ChannelHeader` can also display connection state below the tile which shows the user if they
are connected or offline, etc. on connection events.
To enable this, use the `showConnectionStateTile` property.
```dart
//...
ChannelHeader(
showConnectionStateTile: true,
),
```
@@ -0,0 +1,88 @@
---
id: channel_list_header
sidebar_position: 9
title: ChannelListHeader
---
A Header Widget For A List Of Channels
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListHeader-class.html)
![](../assets/channel_list_header.png)
### Background
A common pattern for most messaging apps is to show a list of Channels (chats) on the first screen
and navigate to an individual one on being clicked. On this first page where the list of channels are
displayed, it is usual to have functionality such as adding a new chat, display the user logged in, etc.
To encapsulate all of this functionality into one widget, the Flutter SDK contains a `ChannelListHeader`
widget which provides these out of the box.
### Basic Example
This is a basic example of a page which has a `ChannelListView` and a `ChannelListHeader` to recreate a
common Channels Page.
```dart
class DemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelListHeader(),
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_('members', [StreamChat.of(context).user.id]),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
),
),
);
}
}
```
### Customizing Parts Of The Header
The header works like a `ListTile` widget.
Use the `titleBuilder`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own.
```dart
//...
ChannelListHeader(
subtitle: Text('My Custom Subtitle'),
),
```
![](../assets/channel_list_header_custom_subtitle.png)
The `titleBuilder` param helps you build different titles depending on the connection state:
```dart
//...
ChannelListHeader(
titleBuilder: (context, status, client) {
switch(status) {
/// Return your title widget
}
},
),
```
### Showing Connection State
The `ChannelListHeader` can also display connection state below the tile which shows the user if they
are connected or offline, etc. on connection events.
To enable this, use the `showConnectionStateTile` property.
```dart
//...
ChannelListHeader(
showConnectionStateTile: true,
),
```
@@ -0,0 +1,111 @@
---
id: channel_list_view
sidebar_position: 4
title: ChannelListView
---
A Widget For Displaying A List Of Channels
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListView-class.html)
![](../assets/channel_list_view.png)
### Background
Channels are fundamental elements of Stream Chat and constitute shared spaces which allow users to
message each other.
1:1 conversations and groups are both examples of channels, albeit with some (distinct/non-distinct)
differences. Displaying the list of channels that a user is a part of is a pattern present in most messaging apps.
The `ChannelListView` widget allows displaying a list of channels to a user. By default, this is NOT
ONLY the channels that the user is a part of. This section goes into setting up and using a `ChannelListView`
widget.
### Basic Example
Here is a basic example of the `ChannelListView` widget. It consists of the main widget itself, a `Filter`
to filter only the channels that the user is a part of, sorting by last message time, pagination params,
and the widget to use when a particular channel is clicked.
```dart
class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_('members', [StreamChat.of(context).user.id]),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
),
),
);
}
}
```
This example by default displays the channels that a user is a part of. Now let's look at customizing
the widget.
### Customizing the Channel Preview
A common aspect of the widget needed to be tweaked according to each app is the Channel Preview (the
Channel tile in the list). To do this, we use the `channelPreviewBuilder` param like this:
```dart
ChannelListView(
...
channelPreviewBuilder: (context, channel) {
return ListTile(
tileColor: Colors.amberAccent,
title: Center(
child: ChannelName(),
),
);
},
),
```
Which gives you a new Channel preview in the list:
![](../assets/channel_preview.png)
### Adding Swipe Actions
To add actions (such as delete, more info, etc) when Channel preview is swiped left, set the `swipeToAction`
parameter to `true`.
```dart
ChannelListView(
...
swipeToAction: true,
),
```
This adds two basic actions - info and delete:
![](../assets/swipe_channel.png)
To add custom actions of your own, use the `swipeActions` param:
```dart
ChannelListView(
...
swipeToAction: true,
swipeActions: [
SwipeAction(
color: Colors.blue,
iconWidget: Icon(Icons.add),
onTap: (channel) {
// Things to do on icon tap
},
),
// Other actions here
]
),
```
@@ -0,0 +1,18 @@
---
id: introduction
sidebar_position: 1
title: Introduction
---
Understanding The UI Package Of The Flutter SDK
### What function does `stream_chat_flutter` serve?
The UI SDK (`stream_chat_flutter`) contains official Flutter components for Stream Chat, a service for building chat applications.
While the Stream Chat service provides the backend for messaging and the LLC provides an easy way to
use it in your Flutter apps, we wanted to make sure that adding Chat functionality to your app was as quick as possible.
The UI package is built on top of the low-level client and the core package and allows you to build a
full fledged app with either the inbuilt components, modify existing components, or easily add widgets
of your own to match your app's style better.
@@ -0,0 +1,170 @@
---
id: message_input
sidebar_position: 6
title: MessageInput
---
A Widget Dealing With Everything Related To Sending A Message
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageInput-class.html)
![](../assets/message_input.png)
### Background
In Stream Chat, we can send messages in a channel. However, sending a message isn't as simple as adding
a `TextField` and logic for sending a message. It involves additional processes like addition of media,
quoting a message, adding a custom command like a GIF board, and much more. Moreover, most apps also
need to customize the input to match their theme, overall color and structure pattern, etc.
To do this, we created a `MessageInput` widget which abstracts all expected functionality a modern input
needs - and allows you to use it out of the box.
### Basic Example
A `StreamChannel` is required above the widget tree in which the `MessageInput` is rendered since the channel is
where the messages sent actually go. Let's look at a common example of how we could use the `MessageInput`:
```dart
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
),
),
MessageInput(),
],
),
);
}
}
```
It is common to put this widget in the same page of a `MessageListView` as the bottom widget.
### Quoting A Message
The quoting functionality allows us to 'reply' to a specific message without creating a thread out of it.
It adds the other message as context when sending a message and also displays it above the sent message.
To quote a message, we provide a `quotedMessage` to the `MessageInput`.
```dart
Message? message;
// ...
MessageInput(
quotedMessage: message,
),
```
This will add the message given above the message about to be sent.
While you can implement your own functionality to select which message to reply to, the `MessageListView`
widget helps in this case since it has an inbuilt `onMessageSwiped` callback which we can use.
```dart
class ChannelPage extends StatefulWidget {
@override
_ChannelPageState createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
Message? quotedMessage;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
// ...
onMessageSwiped: (message) {
setState(() {
quotedMessage = message;
});
},
),
),
MessageInput(
quotedMessage: _quotedMessage,
onQuotedMessageCleared: () {
setState(() => _quotedMessage = null);
},
),
],
),
);
}
}
```
![](../assets/message_input_quoted_message.png)
### Adding Custom Actions
By default, the `MessageInput` has two actions: one for attachments and one for commands like Giphy.
To add your own action, we use the `actions` parameter like this:
```dart
MessageInput(
actions: [
InkWell(
child: Icon(
Icons.location_on,
size: 20.0,
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
// Do something here
},
),
],
),
```
This will add on your action to the existing ones.
### Disable Attachments
To disable attachments being added to the message, set the `disableAttachments` parameter to true.
```dart
MessageInput(
disableAttachments: true,
),
```
### Changing Position Of MessageInput Components
You can also change the position of the TextField, actions and 'send' button relative to each other.
To do this, use the `actionsLocation` or `sendButtonLocation` parameters which help you decide the location
of the buttons in the input.
For example, if we want the actions on the right and the send button inside the TextField, we can do:
```dart
MessageInput(
sendButtonLocation: SendButtonLocation.inside,
actionsLocation: ActionsLocation.right,
),
```
![](../assets/message_input_change_position.png)
@@ -0,0 +1,115 @@
---
id: message_list_view
sidebar_position: 5
title: MessageListView
---
A Widget For Displaying A List Of Messages
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageListView-class.html)
![](../assets/message_list_view.png)
### Background
Every channel can contain a list of messages sent by users inside it. The `MessageListView` widget
displays the list of messages inside a particular channel along with possible attachments and
other message attributes (if the message is pinned for example). This sets it apart from the `MessageSearchListView`
which may not contain messages only from a single channel and is used to search for messages across
many.
### Basic Example
The `MessageListView` shows the list of messages of the current channel. It has inbuilt support for
common messaging functionality: displaying and editing messages, adding / modifying reactions, support
for quoting messages, pinning messages, and more.
An example of how you can use the MessageListView is:
```dart
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
),
),
MessageInput(),
],
),
);
}
}
```
### Enable Threads
Threads are made of a parent message and replies linked to it. To enable threading, the SDK requires you
to supply a `threadBuilder` which will supply the page when the thread is clicked.
```dart
MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
),
```
![](../assets/message_list_view_threads.png)
The `MessageListView` itself can render the thread by supplying the `parentMessage` parameter.
```dart
MessageListView(
parentMessage: parent,
),
```
### Building Custom Messages
You can also supply your own implementation for displaying messages using the `messageBuilder` parameter.
:::note
To customize the existing implementation, look at the `MessageWidget` documentation instead.
:::
```dart
MessageListView(
messageBuilder: (context, details, messageList, defaultImpl) {
// Your implementation of the message here
// E.g: return Text(details.message.text ?? '');
},
),
```
### Enabling Message Pinning
Message pins save and highlight the message in the `MessageListView`. To enable users to pin the message,
make sure the pin permissions are granted for different types of users on the dashboard. After confirming
the appropriate users have permissions, add the user types in the `pinPermissions` parameter.
```dart
MessageListView(
//...
pinPermissions: ['admin', 'userType1', 'userType2'],
),
```
This will allow these user types to pin messages through the message actions modal.
![](../assets/message_list_view_pin.png)
@@ -0,0 +1,62 @@
---
id: message_search_list_view
sidebar_position: 8
title: MessageSearchListView
---
A Widget To Search For Messages Across Channels
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageSearchListView-class.html)
![](../assets/message_search_list_view.png)
### Background
Users in Stream Chat can have several channels and it can get hard to remember which channel has the
message they are searching for. As such, there needs to be a way to search for a message across multiple
channels. This is where `MessageSearchListView` comes in.
### Basic Example
While the MessageListView is tied to a certain `StreamChannel`, a `MessageSearchListView` is not.
```dart
class MessageSearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: MessageSearchBloc(
child: MessageSearchListView(
filters: Filter.in_('members', [StreamChat.of(context).user!.id],),
messageQuery: 'your query here',
paginationParams: PaginationParams(limit: 20),
),
),
);
}
}
```
### Customize The Result Tiles
You can use your own widget for the result items using the `itemBuilder` parameter.
```dart
MessageSearchListView(
// ...
itemBuilder: (context, response) {
return Text(response.message.text);
},
),
```
### Show Result Count
You show the number of results via the `showResultCount` parameter.
```dart
MessageSearchListView(
// ...
showResultCount: true,
),
```
@@ -0,0 +1,98 @@
---
id: message_widget
sidebar_position: 11
title: MessageWidget
---
A Widget For Displaying Messages And Attachments
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageWidget-class.html)
### Background
There are several things that need to be displayed with text in a message in a modern messaging app:
attachments, highlights if the message is pinned, user avatars of the sender, etc.
To encapsulate all of this functionality into one widget, the Flutter SDK contains a `MessageWidget`
widget which provides these out of the box.
### Basic Example (Modifying `MessageWidget` in `MessageListView`)
Primarily, the `MessageWidget` is used in the `MessageListView`. To customize only a few properties
of the `MessageWidget` without supplying all other properties, the `messageBuilder` builder supplies
a default implementation of the widget for us to modify.
```dart
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: MessageListView(
messageBuilder: (context, details, messageList, defaultMessageWidget) {
return defaultMessageWidget.copyWith(
showThreadReplyIndicator: false,
);
},
),
);
}
}
```
### Building A Custom Attachment
When a custom attachment type (location, audio, etc.) is sent, the MessageWidget also needs to know
how to build it. For this purpose, we can use the `customAttachmentBuilders` parameter.
As an example, if a message has a attachment type 'location', we do:
```dart
MessageWidget(
//...
customAttachmentBuilders: {
'location': (context, message, attachments) {
var attachmentWidget = Image.network(
_buildMapAttachment(
attachments[0].extraData['latitude'],
attachments[0].extraData['longitude'],
),
);
return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0));
}
},
)
```
You can also override the builder for existing attachment types like `image` and `video`.
### Show User Avatar For Messages
You can decide to show, hide, or remove user avatars of the sender of the message. To do this, set
the `showUserAvatar` property like this:
```dart
MessageWidget(
//...
showUserAvatar = DisplayWidget.show,
)
```
### Reverse the message
In most cases, `MessageWidget` needs to be a different orientation depending upon if the sender is the
user or someone else.
For this, we use the `reverse` parameter to change the orientation of the message:
```dart
MessageWidget(
//...
reverse = true,
)
```
@@ -0,0 +1,48 @@
---
id: setup
sidebar_position: 2
title: Setup
---
Understanding Setup For `stream_chat_flutter`
### Add pub.dev dependency
First, you need to add the `stream_chat_flutter` dependency to your `pubspec.yaml`.
You can either run this command:
```shell
flutter pub add stream_chat_flutter
```
OR
Add this line in the dependencies section of your pubspec.yaml after substituting latest version:
```yaml
dependencies:
stream_chat_flutter: ^latest_version
```
You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter).
### Details On Platform Support
`stream_chat_flutter` was originally created for Android and iOS mobile platforms. As Flutter matured,
support for additional platforms was added and the package now has experimental support for web and desktop as
[detailed here](https://getstream.io/blog/announcing-experimental-multi-platform-support-for-the-stream-flutter-sdk/).
However, platforms other than mobile may have additional constraints due to not supporting all plugins,
which will be addressed by the respective plugin creators over time.
### Setup: iOS
The library uses [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) to pick files from the os.
Follow [this wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/Setup#ios) to fulfill iOS requirements.
We also use [video_player](https://pub.dev/packages/video_player) to reproduce videos.
Follow [this guide](https://pub.dev/packages/video_player#installation) to fulfill the requirements.
To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin.
Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements.
@@ -0,0 +1,71 @@
---
id: stream_chat_and_theming
sidebar_position: 3
title: StreamChat And Theming
---
Understanding How To Customize Widgets Using `StreamChatTheme`
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatTheme-class.html) and [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatThemeData-class.html)
### Background
Stream's UI SDK makes it easy for developers to add custom styles and attributes to our widgets. Like most Flutter frameworks, Stream exposes a dedicated widget for theming.
Using `StreamChatTheme`, users can customize most aspects of our UI widgets by setting attributes using `StreamChatThemeData`.
Similar to the `Theme` and `ThemeData` in Flutter, Stream Chat uses a top level [inherited widget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) to provide theming information throughout your application. This can be optionally set at the top of your application tree or at a localized point in your widget sub-tree.
If you'd like to customize the look and feel of Stream chat across your entire application, we recommend setting your theme at the top level. Conversely, users can customize specific screens or widgets by wrapping components in a `StreamChatTheme`.
### A closer look at StreamChatThemeData
Looking at the constructor for `StreamChatThemeData`, we can see the full list of properties and widgets available for customization.
Some high-level properties such as `textTheme` or `colorTheme` can be set application-wide directly from this class. In contrast, larger components such as `ChannelHeader`, `MessageInputs`, etc. have been broken up into smaller theme objects.
```dart
factory StreamChatThemeData({
Brightness? brightness,
TextTheme? textTheme,
ColorTheme? colorTheme,
ChannelListHeaderTheme? channelListHeaderTheme,
ChannelPreviewTheme? channelPreviewTheme,
ChannelTheme? channelTheme,
MessageTheme? otherMessageTheme,
MessageTheme? ownMessageTheme,
MessageInputTheme? messageInputTheme,
Widget Function(BuildContext, Channel)? defaultChannelImage,
Widget Function(BuildContext, User)? defaultUserImage,
IconThemeData? primaryIconTheme,
List<ReactionIcon>? reactionIcons,
});
```
### Stream Chat Theme in use
Let's take a look at customizing widgets using `StreamChatTheme`. In the example below, we can change the default color theme to yellow and override the channel header's typography and colors.
```dart
builder: (context, child) => StreamChat(
client: client,
child: child,
streamChatThemeData: StreamChatThemeData(
colorTheme: ColorTheme.light(
primaryAccent: const Color(0xffffe072),
),
channelTheme: ChannelTheme(
channelHeaderTheme: ChannelHeaderTheme(
color: const Color(0xffd34646),
title: TextStyle(
color: Colors.white,
),
),
),
),
),
```
We are creating this class at the very top of our widget tree using the `streamChatThemeData` parameter found in the `StreamChat` widget.
![](../assets/using_theme.jpg)
@@ -0,0 +1,88 @@
---
id: user_list_view
sidebar_position: 7
title: UserListView
---
A Widget For Displaying And Selecting Users
Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/UserListView-class.html)
![](../assets/user_list_view.png)
### Background
A list of users is required for many different purposes: showing a list of users in a Channel,
selecting users to add in a channel, etc. The `UserListView` displays and allows selection of a list
of users along with multiple display configurations like a list and grid.
### Basic Example
Let's take a look at an example where we use the widget to autocomplete user names:
```dart
class UsersListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: UsersBloc(
child: UsersListView(
filter: Filter.notEqual('id', StreamChat.of(context).user!.id),
sort: [
SortOption(
'name',
direction: 1,
),
],
pagination: PaginationParams(
limit: 25,
),
),
),
);
}
}
```
### Customize The User Items
You can use your own widget for the user items using the `userItemBuilder` parameter.
```dart
UsersListView(
// ...
userItemBuilder: (context, user, isSelected) {
return Text(user.name);
},
),
```
### Group Alphabetically
You can group alphabetically using the `groupAlphabetically` parameter:
```dart
UsersListView(
//...
groupAlphabetically: true,
),
```
### Selecting Users
The `UserListView` widget allows selecting users in a list by supplying a selected users list and callbacks
for when user items are tapped.
```dart
Set<User>? selectedUsers = {};
UsersListView(
//...
selectedUsers: selectedUsers,
onUserTap: (user, _) {
setState(() {
selectedUsers.add(user);
});
},
),
```
@@ -0,0 +1,4 @@
{
"label": "Stream Chat Flutter Core",
"position": 4
}
@@ -0,0 +1,68 @@
---
id: channel_list_core
sidebar_position: 4
title: ChannelListCore
---
A Widget For Building A List Of Channels
### Background
The UI SDK of Stream Chat supplies a `ChannelListView` class that builds a list of channels fetching
according to the filters and sort order given. However, in some cases, implementing novel UI is necessary
that cannot be done using the customization approaches given in the widget.
To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that
fetches channels in the expected way via the usual params but does not supply any UI and instead
exposes builders to build the UI in situations such as loading, empty data, errors, and on data received.
### Basic Example
`ChannelListCore` is a simplified class that allows fetching a list of
channels while exposing UI builders.
This allows you to construct your own UI while not having to
worry about the specific logic of fetching channels in your app.
A `ChannelListController` is used to reload and paginate data.
```dart
class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChannelListCore(
filter: Filter.in_(
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
),
errorBuilder: (context, err) {
return Center(
child: Text('An error has occured'),
);
},
emptyBuilder: (context) {
return Center(
child: Text('Nothing here...'),
);
},
loadingBuilder: (context) {
return Center(
child: CircularProgressIndicator(),
);
},
listBuilder: (context, list) {
return ChannelPage(list);
}
),
);
}
}
```
Make sure to have a `StreamChatCore` ancestor in order to provide the
information about the channels.
@@ -0,0 +1,90 @@
---
id: channels_bloc
sidebar_position: 7
title: ChannelsBloc
---
A Widget Dedicated To The Management Of A Channel List With Pagination.
### Background
Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels,
messages, users, etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current
data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or
existing data and build UI based on it.
All Core and UI widgets which focus on fetching a list of objects need to have their respective functions
above them in the widget tree. The ChannelListCore and ChannelListView require the ChannelsBloc
above them in the widget hierarchy without which they will fail.
### Understanding The Widget
`ChannelsBloc` is used together with `ChannelListCore` to manage a list of
Channels with pagination, re-ordering, querying and other operations
associated with Channels.
`ChannelsBloc` can be accessed at anytime by using the static `.of` method
using Flutter's `BuildContext`.
```dart
var _channelsBloc = ChannelsBloc.of(context);
```
The `ChannelsBloc` widget encapsulates common functionality related to channel lists such as fetching
the existing channels and querying new channels and also supplies them down the widget tree.
The widget is required for the respective core widget (`ChannelListCore`) to fetch channels and hence
must be above the core widget in the tree.
Here is a basic implementation of `ChannelsBloc`:
```dart
ChannelsBloc(
child: // Further Widget Tree
),
```
The `ChannelsBloc` widget allows three customisations:
#### Lock Channels Order
ChannelsBloc may change the order of channels when new messages arrive. To lock this order, we can
set the `lockChannelsOrder` property to true.
```dart
ChannelsBloc(
lockChannelsOrder: true,
child: // Further Widget Tree
),
```
#### Set custom channel order
We can decide the order of the channels in the list by supplying a comparator to the `channelsComparator`
parameter:
```dart
ChannelsBloc(
channelsComparator: (a, b) {
return a.createdAt!.millisecondsSinceEpoch >
b.createdAt!.millisecondsSinceEpoch
? 1
: -1;
},
child: // Further Widget Tree
),
```
#### Decide if channel should be added on new message event
When a new message arrives, a `message.new` event is received. We can decide if we want to add the channel
to the list using the `shouldAddChannel` parameter which is a callback supplying the event data:
```dart
ChannelsBloc(
shouldAddChannel: (event) {
return event.message!.extraData['priority'] == '1';
},
child: // Further Widget Tree
),
```
@@ -0,0 +1,80 @@
---
id: introduction
sidebar_position: 1
title: Introduction
---
Understanding The Core Package Of The Flutter SDK
This package provides business logic to fetch common things required for integrating Stream Chat into your application.
The core package allows more customisation and hence provides business logic but no UI components.
Please use the `stream_chat_flutter` package for the full fledged suite of UI components or `stream_chat` for the low-level client.
### Background
In the early days of the Flutter SDK, the SDK was only split into the LLC (`stream_chat`) and
the UI package (`stream_chat_flutter`). With this you could use a fully built interface with the UI package
or a fully custom interface with the LLC. However, we soon recognised the need for a third intermediary
package which made tasks like building and modifying a list of channels or messages easy but without
the complexity of using low level components. The Core package (`stream_chat_flutter_core`) is a manifestation
of the same idea and allows you to build an interface with Stream Chat without having to deal with
low level code and architecture as well as implementing your own theme and UI effortlessly.
Also, it has very few dependencies.
We will now explore the components of this intermediary package and understand how it helps you build
the experience you want your users to have.
The package primarily contains three types of classes:
* Business Logic Components
* Core Components
* Core Controllers
### Business Logic Components
These components allow you to have the maximum and lower-level control of the queries being executed.
In BLoCs, the basic functionalities - such as queries for messages, channels or queries - are bundled up
and passed along down the tree. Using a BLoC allows you to either create your own way to fetch and
build UIs or use an inbuilt Core widget to do the work such as queries, pagination, etc for you.
The BLoCs we provide are:
* ChannelsBloc
* MessageSearchBloc
* UsersBloc
### Core Components
Core components usually are an easy way to fetch data associated with Stream Chat.
Core components use functions exposed by the respective BLoCs (for example the ChannelListCore uses the ChannelsBloc)
and use the respective controllers for various operations. Unlike heavier components from the UI
package, core components are decoupled from UI and they expose builders instead to help you build
a fully custom interface.
Data fetching can be controlled with the controllers of the respective core components.
* ChannelListCore (Fetch a list of channels)
* MessageListCore (Fetch a list of messages from a channel)
* MessageSearchListCore (Fetch a list of search messages)
* UserListCore (Fetch a list of users)
* StreamChatCore (This is different from the other core components - it is a version of StreamChat decoupled from theme and initialisations.)
### Core Controllers
Core Controllers are supplied to respective CoreList widgets which allows reloading and pagination of data whenever needed.
Unlike the UI package, the Core package allows a fully custom user interface built with the data. This
in turn provides a few challenges: we do not know implicitly when to paginate your list or reload your data.
While this is handled out of the box in the UI package since the List implementation is inbuilt, a controller
needs to be used in the core package notifying the core components to reload or paginate the data existing
currently. For this, each core component has a respective controller which you can use to call the
specific function (reload / paginate) whenever such an event is triggered through / needed in your UI.
* ChannelListController
* MessageListController
* MessageSearchListController
* ChannelListController
This section goes into the individual core package widgets and their functional use.
@@ -0,0 +1,70 @@
---
id: message_list_core
sidebar_position: 5
title: MessageListCore
---
A Widget For Building A List Of Messages
### Background
The UI SDK of Stream Chat supplies a `MessageListView` class that builds a list of channels fetching
according to the filters and sort order given. However, in some cases, implementing novel UI is necessary
that cannot be done using the customization approaches given in the widget.
To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that
fetches channels in the expected way via the usual params but does not supply any UI and instead
exposes builders to build the UI in situations such as loading, empty data, errors, and on data received.
### Basic Example
`MessageListCore` is a simplified class that allows fetching a list of
messages while exposing UI builders.
This allows you to construct your own UI while not having to
worry about the specific logic of fetching messages in a channel.
A `MessageListController` is used to paginate data.
```dart
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: MessageListCore(
emptyBuilder: (context) {
return Center(
child: Text('Nothing here...'),
);
},
loadingBuilder: (context) {
return Center(
child: CircularProgressIndicator(),
);
},
messageListBuilder: (context, list) {
return MessagesPage(list);
},
errorWidgetBuilder: (context, err) {
return Center(
child: Text('Error'),
);
},
),
),
],
),
);
}
}
```
Make sure to have a `StreamChannel` ancestor in order to provide the
information about the channels.
@@ -0,0 +1,40 @@
---
id: message_search_list_block
sidebar_position: 8
title: MessageSearchListBloc
---
A Widget Used To Manage A List Of Messages With Pagination.
### Background
Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels,
messages, users etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current
data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or
existing data and build UI based on it.
All Core and UI widgets which focus on fetching a list of objects need to have their respective functions
above them in the widget tree. The MessageSearchListCore and MessageSearchListView require the
MessageSearchListCore above them in the widget hierarchy without which they will fail.
### Understanding The Widget
This class can be used to load messages, perform queries, etc.
`MessageSearchBloc` can be accessed at anytime by using the static `.of` method
using Flutter's BuildContext.
```dart
var _searchBloc = MessageSearchBloc.of(context);
```
The `MessageSearchBloc` widget encapsulates common functionality related to searching for messages
across channels and also supplies them down the widget tree.
Here is a basic implementation of `ChannelsBloc`:
```dart
MessageSearchBloc(
child: // Further Widget Tree
),
```
@@ -0,0 +1,41 @@
---
id: message_search_list_core
sidebar_position: 6
title: MessageSearchListCore
---
A Widget For Displaying Message Searches
### Background
The UI SDK of Stream Chat supplies a `MessageSearchListView` class that builds a list of channels fetching
according to the filters and sort order given. However, in some cases, implementing novel UI is necessary
that cannot be done using the customization approaches given in the widget.
To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that
fetches channels in the expected way via the usual params but does not supply any UI and instead
exposes builders to build the UI in situations such as loading, empty data, errors, and on data received.
### Basic Example
`MessageSearchListCore` is a simplified class that allows searching for
messages across channels while exposing UI builders.
A `MessageSearchListController` is used to load and paginate data.
```dart
class MessageSearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: MessageSearchListCore(
messageQuery: _messageFilter,
filters: _channelsFilter,
paginationParams: PaginationParams(limit: 20),
),
);
}
}
```
Make sure to have a `MessageSearchBloc` ancestor in order to provide the
information about the messages.
@@ -0,0 +1,28 @@
---
id: setup
sidebar_position: 2
title: Setup
---
Understanding Setup For `stream_chat_flutter_core`
### Add pub.dev dependency
First, you need to add the `stream_chat_flutter_core` dependency to your pubspec.yaml
You can either run this command:
```shell
flutter pub add stream_chat_flutter_core
```
OR
Add this line in the dependencies section of your pubspec.yaml after substituting latest version:
```yaml
dependencies:
stream_chat_flutter_core: ^latest_version
```
You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter_core).
@@ -0,0 +1,26 @@
---
id: stream_chat_core
sidebar_position: 3
title: StreamChatCore
---
`StreamChatCore` is a version of `StreamChat` found in `stream_chat_flutter` that is decoupled from
theme and initialisations.
`StreamChatCore` is used to provide information about the chat client to the widget tree.
This Widget is used to react to life cycle changes and system updates.
When the app goes into the background, the websocket connection is automatically closed and when it goes back to foreground the connection is opened again.
Like the `StreamChat` widget in the higher level UI package, the `StreamChatCore` widget should
be on the top level before using any Stream functionality:
```dart
return MaterialApp(
title: 'Stream Chat Core Example',
home: HomeScreen(),
builder: (context, child) => StreamChatCore(
client: client,
child: child!,
),
);
```
@@ -0,0 +1,64 @@
---
id: user_list_core
sidebar_position: 10
title: UserListCore
---
A Widget For Building A List Of Users
### Background
The UI SDK of Stream Chat supplies a `UserListView` class that builds a list of channels fetching
according to the filters and sort order given. However, in some cases, implementing novel UI is necessary
that cannot be done using the customization approaches given in the widget.
To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that
fetches channels in the expected way via the usual params but does not supply any UI and instead
exposes builders to build the UI in situations such as loading, empty data, errors, and on data received.
### Basic Example
`UserListCore` is a simplified class that allows fetching users while
exposing UI builders.
A `UserListController` is used to load and paginate data.
```dart
class UsersListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: UsersListCore(
sort: [SortOption('last_active')],
pagination: PaginationParams(
limit: 20,
),
errorBuilder: (err) {
return Center(
child: Text('An error has occured'),
);
},
emptyBuilder: (context) {
return Center(
child: Text('Nothing here...'),
);
},
emptyBuilder: (context) {
return Center(
child: CircularProgressIndicator(),
);
},
listBuilder: (context, list) {
return UsersPage(list);
}
),
);
}
}
```
`UsersBloc` must be the ancestor of this widget. This is necessary since
`UserListCore` depends on functionality contained within `UsersBloc`.
The parameters `listBuilder`, `loadingBuilder`, `emptyBuilder` and
`errorBuilder` must all be supplied and not null.
@@ -0,0 +1,37 @@
---
id: users_bloc
sidebar_position: 9
title: UsersBloc
---
A Widget Dedicated To The Management Of A Users List With Pagination.
### Background
Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels,
messages, users, etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current
data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or
existing data and build UI based on it.
All Core and UI widgets which focus on fetching a list of objects need to have their respective functions
above them in the widget tree. The UserListCore and UserListView require the UserListCore
above them in the widget hierarchy without which they will fail.
### Understanding The Widget
`UsersBloc` can be accessed at anytime by using the static `.of` method
using Flutter's `BuildContext`.
```dart
var _userBloc_ = UsersBloc.of(context);
```
The `UsersBloc` widget encapsulates common functionality related to user lists and also supplies them down the widget tree.
Here is a basic implementation of `UsersBloc`:
```dart
UsersBloc(
child: // Further Widget Tree
),
```
+54 -43
View File
@@ -1,4 +1,4 @@
name: stream_chat_dart
name: stream_chat_flutter
versioning:
mode: independent
@@ -7,57 +7,68 @@ packages:
- packages/**
scripts:
lint:all:
run: melos run analyze && melos run format
description: Run all static analysis checks
# - Requires `pub global activate tuneup`.
analyze: >
melos exec -c 1 --fail-fast -- \
pub global run tuneup check
analyze:
run: |
melos exec -c 4 --ignore="*example*" -- \
dart analyze --fatal-infos .
description: |
Run `dart analyze` in all packages.
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
format: pub global run flutter_plugin_tools format
format:
run: flutter format --set-exit-if-changed .
description: |
Run `flutter format --set-exit-if-changed .` in all packages.
lint:pub:
run: |
melos exec -c 4 --no-private --ignore="*example*" -- \
pub publish --dry-run
description: |
Run `pub publish --dry-run` in all packages.
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
build:examples:ios: >
melos exec -c 1 --scope="*example*" --fail-fast -- \
flutter build ios --no-codesign
generate:all:
run: melos run generate:dart && melos run generate:flutter
description: Build all generated files for Dart & Flutter packages in this project.
generate:dart:
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs"
description: Build all generated files for Dart packages in this project.
build:examples:android: >
melos exec -c 1 --scope="*example*" --fail-fast -- \
flutter build apk
generate:flutter:
run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs"
description: Build all generated files for Flutter packages in this project.
# Build any plugin example apps that have MacOS support.
# - Requires `flutter config --enable-macos-desktop` enabled.
# - Requires `flutter channel master && flutter upgrade`.
build:examples:macos: >
melos exec -c 1 --scope="*example*" --dir-exists=macos --fail-fast -- \
flutter build macos
test:all:
run: melos run test:dart --no-select && melos run test:flutter --no-select
description: Run all Dart & Flutter tests in this project.
test:dart:
run: melos exec -c 1 --fail-fast -- "flutter test --coverage"
description: Run Dart tests for a specific package in this project.
select-package:
flutter: false
dir-exists: test
test:dart: >
melos exec -c 1 --fail-fast --no-flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter pub run test
test:flutter:
run: melos exec -c 3 --fail-fast -- "flutter test --coverage"
description: Run Flutter tests for a specific package in this project.
select-package:
flutter: true
dir-exists: test
test:flutter: >
melos exec -c 1 --fail-fast --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test
test:web: >
melos exec -c 1 --fail-fast --dir-exists=test --scope="*web*" -- \
flutter test --platform=chrome
lint:pub: >
melos exec -c 5 --fail-fast --no-private --ignore="*example*" -- \
pub publish --dry-run
postclean: >
melos exec -- \
rm -rf ./build ./android/.gradle ./ios/.symlinks ./ios/Pods ./android/.idea ./.idea ./.dart-tool/build
dev_dependencies:
pedantic: 1.9.2
coverage:ignore-file:
run: |
melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh"
description: Removes all the ignored files from the coverage report.
select-package:
dir-exists: coverage
environment:
sdk: ">=2.7.0 <3.0.0"
flutter: ">=1.22.4 <2.0.0"
sdk: '>=2.12.0 <3.0.0'
flutter: '>=1.22.4 <2.0.0'
+110 -7
View File
@@ -1,3 +1,100 @@
## 2.0.0
🛑️ Breaking Changes from `1.5.3`
- migrate this package to null safety
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor)
- `client.disconnect()` is now divided into two different functions
- `client.closeConnection()` -> for closing user websocket connection.
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
- `client.devToken()` now returns a `Token` model instead of `String`.
- `ApiError` is removed in favor of `StreamChatError`
- `StreamChatError` -> parent type for all the stream errors.
- `StreamWebSocketError` -> for user websocket related errors.
- `StreamChatNetworkError` -> for network related errors.
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
- `option.state` -> bool state
- `option.watch` -> bool watch
- `option.presence` -> bool presence
- `client.queryUsers()` options param is removed in favor of individual params
- `option.presence` -> bool presence
- Migrate this package to null safety
- Added typed filters
🐞 Fixed
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection
- several minor fixes
- performance improvements
✅ Added
- New `Location` enum is introduced for easily changing the client location/baseUrl.
- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection.
- New `client.partialUpdateMessage` and `channel.partialUpdateMessage` methods
- `connectWebSocket` parameter in connect user calls to use the client in "connection-less" mode.
🔄 Changed
- `baseURL` is now deprecated in favor of using `Location` to change data location.
## 2.0.0-nullsafety.8
🐞 Fixed
- Export `PushProvider` enum
## 2.0.0-nullsafety.7
🛑️ Breaking Changes from `2.0.0-nullsafety.6`
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor)
- `client.disconnect()` is now divided into two different functions
- `client.closeConnection()` -> for closing user websocket connection.
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
- `client.devToken()` now returns a `Token` model instead of `String`.
- `ApiError` is removed in favor of `StreamChatError`
- `StreamChatError` -> parent type for all the stream errors.
- `StreamWebSocketError` -> for user websocket related errors.
- `StreamChatNetworkError` -> for network related errors.
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
- `option.state` -> bool state
- `option.watch` -> bool watch
- `option.presence` -> bool presence
- `client.queryUsers()` options param is removed in favor of individual params
- `option.presence` -> bool presence
✅ Added
- New `Location` enum is introduced for easily changing the client location/baseUrl.
- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection.
🔄 Changed
- `baseURL` is now deprecated in favor of using `Location` to change data location.
## 2.0.0-nullsafety.6
- Fix thread reply not working with attachments
- Minor fixes
## 2.0.0-nullsafety.5
- Minor fixes
- Performance improvements
- Fixed `skip_push` in `client.sendMessage`
- Added partial message update method
## 2.0.0-nullsafety.2
- Added new `Filter.raw` constructor
- Changed extraData
- Minor fixes
## 2.0.0-nullsafety.1
- Migrate this package to null safety
- Added typed filters
## 1.5.3
- fix: `StreamChatClient.connect` returns quicker when you're using the persistence package
@@ -34,7 +131,8 @@
- Save pinned messages in offline storage
- Minor fixes
- `StreamClient.QueryChannels` now returns a Stream and fetches the channels from storage before calling the api
- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or offline
- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or
offline
## 1.2.0-beta
@@ -45,7 +143,8 @@
## 1.1.0-beta
- Fixed minor bugs
- Add support for custom attachment upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart)
- Add support for custom attachment
upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart)
- Add support for asynchronous attachment upload
## 1.0.3-beta
@@ -55,7 +154,8 @@
## 1.0.2-beta
- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser`, `connectUserWithProvider`
- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser`
, `connectUserWithProvider`
- Optimised reaction updates - i.e., Update first call Api later.
## 1.0.1-beta
@@ -65,9 +165,11 @@
## 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** 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`
- 🛑 **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
@@ -122,7 +224,8 @@
## 0.2.20
- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we cannot sort by custom properties.
- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we
cannot sort by custom properties.
## 0.2.19
@@ -136,7 +239,7 @@
## 0.2.17+1
- Do not retry messages when server returns error
- Do not retry messages when server returns error
## 0.2.17
-144
View File
@@ -1,144 +0,0 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
+1 -2
View File
@@ -4,5 +4,4 @@ targets:
json_serializable:
options:
explicit_to_json: true
field_rename: snake
any_map: true
field_rename: snake
@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
location = "self:">
</FileRef>
</Workspace>
+105 -107
View File
@@ -2,12 +2,9 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
Future<void> main() async {
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
/// project dashboard.
final client = StreamChatClient(
'b67pax5b2wdq',
logLevel: Level.INFO,
);
/// Create a new instance of [StreamChatClient]
/// by passing the apikey obtained from your project dashboard.
final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO);
/// Set the current user. In a production scenario, this should be done using
/// a backend to generate a user token using our server SDK.
@@ -16,12 +13,12 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: {
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''',
);
/// Creates a channel using the type `messaging` and `godevs`.
@@ -44,55 +41,57 @@ Future<void> main() async {
/// Example using Stream's Low Level Dart client.
class StreamExample extends StatelessWidget {
/// To initialize this example, an instance of [client] and [channel] is required.
/// To initialize this example, an instance of
/// [client] and [channel] is required.
const StreamExample({
Key key,
@required this.client,
@required this.channel,
Key? key,
required this.client,
required this.channel,
}) : super(key: key);
/// Instance of [StreamChatClient] we created earlier. This contains information about
/// our application and connection state.
/// Instance of [StreamChatClient] we created earlier.
/// This contains information about our application and connection state.
final StreamChatClient client;
/// The channel we'd like to observe and participate.
final Channel channel;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stream Chat Dart Example',
home: HomeScreen(channel: channel),
);
}
Widget build(BuildContext context) => MaterialApp(
title: 'Stream Chat Dart Example',
home: HomeScreen(channel: channel),
);
}
/// Main screen of our application. The layout is comprised of an [AppBar]
/// containing the channel name and a [MessageView] displaying recent messages.
class HomeScreen extends StatelessWidget {
/// [HomeScreen] is constructed using the [Channel] we defined earlier.
const HomeScreen({Key key, @required this.channel}) : super(key: key);
const HomeScreen({
Key? key,
required this.channel,
}) : super(key: key);
/// Channel object containing the [Channel.id] we'd like to observe.
final Channel channel;
@override
Widget build(BuildContext context) {
final messages = channel.state.channelStateStream;
final messages = channel.state!.messagesStream;
return Scaffold(
appBar: AppBar(
title: Text('Channel: ${channel.id}'),
),
body: SafeArea(
child: StreamBuilder<ChannelState>(
child: StreamBuilder<List<Message>?>(
stream: messages,
builder: (
BuildContext context,
AsyncSnapshot<ChannelState> snapshot,
AsyncSnapshot<List<Message>?> snapshot,
) {
if (snapshot.hasData && snapshot.data != null) {
return MessageView(
messages: snapshot.data.messages.reversed.toList(),
messages: snapshot.data!.reversed.toList(),
channel: channel,
);
} else if (snapshot.hasError) {
@@ -104,8 +103,8 @@ class HomeScreen extends StatelessWidget {
}
return const Center(
child: SizedBox(
width: 100.0,
height: 100.0,
width: 100,
height: 100,
child: CircularProgressIndicator(),
),
);
@@ -121,9 +120,9 @@ class HomeScreen extends StatelessWidget {
class MessageView extends StatefulWidget {
/// Message takes the latest list of messages and the current channel.
const MessageView({
Key key,
@required this.messages,
@required this.channel,
Key? key,
required this.messages,
required this.channel,
}) : super(key: key);
/// List of messages sent in the given channel.
@@ -137,8 +136,8 @@ class MessageView extends StatefulWidget {
}
class _MessageViewState extends State<MessageView> {
TextEditingController _controller;
ScrollController _scrollController;
late final TextEditingController _controller;
late final ScrollController _scrollController;
List<Message> get _messages => widget.messages;
@@ -166,86 +165,85 @@ class _MessageViewState extends State<MessageView> {
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = _messages[index];
if (item.user.id == widget.channel.client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.text),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.text),
),
);
}
},
Widget build(BuildContext context) => Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = _messages[index];
if (item.user?.id == widget.channel.client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
}
},
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
),
),
Material(
type: MaterialType.circle,
color: Colors.blue,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () async {
// We can send a new message by calling `sendMessage` on
// the current channel. After sending a message, the
// TextField is cleared and the list view is scrolled
// to show the new item.
if (_controller.value.text.isNotEmpty) {
await widget.channel.sendMessage(
Message(text: _controller.value.text),
);
_controller.clear();
_updateList();
}
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: Icon(
Icons.send,
color: Colors.white,
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
),
),
)
],
),
)
],
);
}
Material(
type: MaterialType.circle,
color: Colors.blue,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () async {
// We can send a new message by calling `sendMessage` on
// the current channel. After sending a message, the
// TextField is cleared and the list view is scrolled
// to show the new item.
if (_controller.value.text.isNotEmpty) {
await widget.channel.sendMessage(
Message(text: _controller.value.text),
);
_controller.clear();
_updateList();
}
},
child: const Padding(
padding: EdgeInsets.all(8),
child: Center(
child: Icon(
Icons.send,
color: Colors.white,
),
),
),
),
)
],
),
)
],
);
}
/// Helper extension for quickly retrieving the current user id from a [StreamChatClient].
/// Helper extension for quickly retrieving
/// the current user id from a [StreamChatClient].
extension on StreamChatClient {
String get uid => state.user.id;
String get uid => state.user!.id;
}
+6 -5
View File
@@ -1,21 +1,22 @@
name: example
description: A new Flutter project.
publish_to: 'none'
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
sdk: '>=2.12.0 <3.0.0'
dependencies:
cupertino_icons: ^1.0.0
flutter:
sdk: flutter
cupertino_icons: ^1.0.0
stream_chat:
stream_chat:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
uses-material-design: true
@@ -1,399 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'responses.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SyncResponse _$SyncResponseFromJson(Map json) {
return SyncResponse()
..duration = json['duration'] as String
..events = (json['events'] as List)
?.map((e) => e == null
? null
: Event.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
QueryChannelsResponse _$QueryChannelsResponseFromJson(Map json) {
return QueryChannelsResponse()
..duration = json['duration'] as String
..channels = (json['channels'] as List)
?.map((e) => e == null ? null : ChannelState.fromJson(e as Map))
?.toList();
}
TranslateMessageResponse _$TranslateMessageResponseFromJson(Map json) {
return TranslateMessageResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: TranslatedMessage.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
QueryMembersResponse _$QueryMembersResponseFromJson(Map json) {
return QueryMembersResponse()
..duration = json['duration'] as String
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
QueryUsersResponse _$QueryUsersResponseFromJson(Map json) {
return QueryUsersResponse()
..duration = json['duration'] as String
..users = (json['users'] as List)
?.map((e) => e == null
? null
: User.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
QueryReactionsResponse _$QueryReactionsResponseFromJson(Map json) {
return QueryReactionsResponse()
..duration = json['duration'] as String
..reactions = (json['reactions'] as List)
?.map((e) => e == null
? null
: Reaction.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
QueryRepliesResponse _$QueryRepliesResponseFromJson(Map json) {
return QueryRepliesResponse()
..duration = json['duration'] as String
..messages = (json['messages'] as List)
?.map((e) => e == null
? null
: Message.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
ListDevicesResponse _$ListDevicesResponseFromJson(Map json) {
return ListDevicesResponse()
..duration = json['duration'] as String
..devices = (json['devices'] as List)
?.map((e) => e == null
? null
: Device.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
SendFileResponse _$SendFileResponseFromJson(Map json) {
return SendFileResponse()
..duration = json['duration'] as String
..file = json['file'] as String;
}
SendImageResponse _$SendImageResponseFromJson(Map json) {
return SendImageResponse()
..duration = json['duration'] as String
..file = json['file'] as String;
}
SendReactionResponse _$SendReactionResponseFromJson(Map json) {
return SendReactionResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..reaction = json['reaction'] == null
? null
: Reaction.fromJson((json['reaction'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson(Map json) {
return ConnectGuestUserResponse()
..duration = json['duration'] as String
..accessToken = json['access_token'] as String
..user = json['user'] == null
? null
: User.fromJson((json['user'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) {
return UpdateUsersResponse()
..duration = json['duration'] as String
..users = (json['users'] as Map)?.map(
(k, e) => MapEntry(
k as String,
e == null
? null
: User.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
))),
);
}
UpdateMessageResponse _$UpdateMessageResponseFromJson(Map json) {
return UpdateMessageResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
SendMessageResponse _$SendMessageResponseFromJson(Map json) {
return SendMessageResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
GetMessageResponse _$GetMessageResponseFromJson(Map json) {
return GetMessageResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
SearchMessagesResponse _$SearchMessagesResponseFromJson(Map json) {
return SearchMessagesResponse()
..duration = json['duration'] as String
..results = (json['results'] as List)
?.map((e) => e == null ? null : GetMessageResponse.fromJson(e as Map))
?.toList();
}
GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(Map json) {
return GetMessagesByIdResponse()
..duration = json['duration'] as String
..messages = (json['messages'] as List)
?.map((e) => e == null
? null
: Message.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
UpdateChannelResponse _$UpdateChannelResponseFromJson(Map json) {
return UpdateChannelResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson(Map json) {
return PartialUpdateChannelResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
InviteMembersResponse _$InviteMembersResponseFromJson(Map json) {
return InviteMembersResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
RemoveMembersResponse _$RemoveMembersResponseFromJson(Map json) {
return RemoveMembersResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
SendActionResponse _$SendActionResponseFromJson(Map json) {
return SendActionResponse()
..duration = json['duration'] as String
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
AddMembersResponse _$AddMembersResponseFromJson(Map json) {
return AddMembersResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
AcceptInviteResponse _$AcceptInviteResponseFromJson(Map json) {
return AcceptInviteResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
RejectInviteResponse _$RejectInviteResponseFromJson(Map json) {
return RejectInviteResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..message = json['message'] == null
? null
: Message.fromJson((json['message'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
));
}
EmptyResponse _$EmptyResponseFromJson(Map json) {
return EmptyResponse()..duration = json['duration'] as String;
}
ChannelStateResponse _$ChannelStateResponseFromJson(Map json) {
return ChannelStateResponse()
..duration = json['duration'] as String
..channel = json['channel'] == null
? null
: ChannelModel.fromJson((json['channel'] as Map)?.map(
(k, e) => MapEntry(k as String, e),
))
..messages = (json['messages'] as List)
?.map((e) => e == null
? null
: Message.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..members = (json['members'] as List)
?.map((e) => e == null
? null
: Member.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList()
..watcherCount = json['watcher_count'] as int
..read = (json['read'] as List)
?.map((e) => e == null
? null
: Read.fromJson((e as Map)?.map(
(k, e) => MapEntry(k as String, e),
)))
?.toList();
}
@@ -1,39 +0,0 @@
import 'package:meta/meta.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart';
/// The retry options
class RetryPolicy {
/// Instantiate a new RetryPolicy
RetryPolicy({
@required this.shouldRetry,
@required this.retryTimeout,
this.attempt,
});
/// The number of attempts tried so far
int attempt = 0;
/// This function evaluates if we should retry the failure
final bool Function(StreamChatClient client, int attempt, ApiError apiError)
shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout
/// method is called to determine the timeout
final Duration Function(
StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
RetryPolicy copyWith({
bool Function(StreamChatClient client, int attempt, ApiError apiError)
shouldRetry,
Duration Function(StreamChatClient client, int attempt, ApiError apiError)
retryTimeout,
int attempt,
}) =>
RetryPolicy(
retryTimeout: retryTimeout ?? this.retryTimeout,
shouldRetry: shouldRetry ?? this.shouldRetry,
attempt: attempt ?? this.attempt,
);
}
@@ -1,194 +0,0 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat/src/api/channel.dart';
import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
class RetryQueue {
/// Instantiate a new RetryQueue object
RetryQueue({
@required this.channel,
this.logger,
}) {
_retryPolicy = channel.client.retryPolicy;
_listenConnectionRecovered();
_listenFailedEvents();
}
/// The channel of this queue
final Channel channel;
/// The logger associated to this queue
final Logger logger;
final _subscriptions = <StreamSubscription>[];
void _listenConnectionRecovered() {
_subscriptions
.add(channel.client.on(EventType.connectionRecovered).listen((event) {
if (!_isRetrying && event.online) {
_startRetrying();
}
}));
}
final HeapPriorityQueue<Message> _messageQueue = HeapPriorityQueue(_byDate);
bool _isRetrying = false;
RetryPolicy _retryPolicy;
/// Add a list of messages
void add(List<Message> messages) {
logger?.info('added ${messages.length} messages');
final messageList = _messageQueue.toList();
_messageQueue.addAll(messages
.where((element) => !messageList.any((m) => m.id == element.id)));
if (_messageQueue.isNotEmpty && !_isRetrying) {
_startRetrying();
}
}
Future<void> _startRetrying() async {
logger?.info('start retrying');
_isRetrying = true;
final retryPolicy = _retryPolicy.copyWith(attempt: 0);
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.first;
try {
logger?.info('retry attempt ${retryPolicy.attempt}');
await _sendMessage(message);
logger?.info('message sent - removing it from the queue');
_messageQueue.remove(message);
logger?.info('now ${_messageQueue.length} messages in the queue');
retryPolicy.attempt = 0;
} catch (error) {
ApiError apiError;
if (error is DioError) {
if (error.type == DioErrorType.RESPONSE) {
_messageQueue.remove(message);
return;
}
apiError = ApiError(
error.response?.data,
error.response?.statusCode,
);
} else if (error is ApiError) {
apiError = error;
if (apiError.status?.toString()?.startsWith('4') == true) {
_messageQueue.remove(message);
return;
}
}
if (!retryPolicy.shouldRetry(
channel.client,
retryPolicy.attempt,
apiError,
)) {
_messageQueue.toList().forEach(_sendFailedEvent);
_isRetrying = false;
return;
}
retryPolicy.attempt++;
final timeout = retryPolicy.retryTimeout(
channel.client,
retryPolicy.attempt,
apiError,
);
await Future.delayed(timeout);
}
}
_isRetrying = false;
}
void _sendFailedEvent(Message message) {
final newStatus = message.status == MessageSendingStatus.sending
? MessageSendingStatus.failed
: (message.status == MessageSendingStatus.updating
? MessageSendingStatus.failed_update
: MessageSendingStatus.failed_delete);
channel.state.addMessage(message.copyWith(
status: newStatus,
));
}
Future<void> _sendMessage(Message message) async {
if (message.status == MessageSendingStatus.failed_update ||
message.status == MessageSendingStatus.updating) {
await channel.updateMessage(message);
} else if (message.status == MessageSendingStatus.failed ||
message.status == MessageSendingStatus.sending) {
await channel.sendMessage(message);
} else if (message.status == MessageSendingStatus.failed_delete ||
message.status == MessageSendingStatus.deleting) {
await channel.deleteMessage(message);
}
}
void _listenFailedEvents() {
_subscriptions.add(channel.on().listen((event) {
final messageList = _messageQueue.toList();
if (event.message != null) {
final messageIndex =
messageList.indexWhere((m) => m.id == event.message.id);
if (messageIndex == -1 &&
[
MessageSendingStatus.failed_update,
MessageSendingStatus.failed,
MessageSendingStatus.failed_delete,
].contains(event.message.status)) {
logger?.info('add message from events');
add([event.message]);
} else if (messageIndex != -1 &&
[
MessageSendingStatus.sent,
null,
].contains(event.message.status)) {
_messageQueue.remove(messageList[messageIndex]);
}
}
}));
}
/// Call this method to dispose this object
void dispose() {
_messageQueue.clear();
_subscriptions.forEach((s) => s.cancel());
}
static int _byDate(Message m1, Message m2) {
final date1 = _getMessageDate(m1);
final date2 = _getMessageDate(m2);
return date1.compareTo(date2);
}
static DateTime _getMessageDate(Message m1) {
switch (m1.status) {
case MessageSendingStatus.failed_delete:
case MessageSendingStatus.deleting:
return m1.deletedAt;
case MessageSendingStatus.failed:
case MessageSendingStatus.sending:
return m1.createdAt;
case MessageSendingStatus.failed_update:
case MessageSendingStatus.updating:
return m1.updatedAt;
default:
return null;
}
}
}
@@ -1,7 +0,0 @@
import 'package:web_socket_channel/html.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Html version of websocket implementation
/// Used in Flutter web version
WebSocketChannel connectWebSocket(String url, {Iterable<String> protocols}) =>
HtmlWebSocketChannel.connect(url, protocols: protocols);
@@ -1,7 +0,0 @@
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// IO version of websocket implementation
/// Used in Flutter mobile version
WebSocketChannel connectWebSocket(String url, {Iterable<String> protocols}) =>
IOWebSocketChannel.connect(url, protocols: protocols);
@@ -1,9 +0,0 @@
import 'package:web_socket_channel/web_socket_channel.dart';
/// Stub version of websocket implementation
/// Used just for conditional library import
WebSocketChannel connectWebSocket(String url,
{Iterable<String> protocols,
Map<String, dynamic> headers,
Duration pingInterval}) =>
throw UnimplementedError();
@@ -1,323 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/connection_status.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Typedef which exposes an [Event] as the only parameter.
typedef EventHandler = void Function(Event);
/// Typedef used for connecting to a websocket. Method returns a
/// [WebSocketChannel] and accepts a connection [url] and an optional
/// [Iterable] of `protocols`.
typedef ConnectWebSocket = WebSocketChannel Function(String url,
{Iterable<String> protocols});
// TODO: parse error even
// TODO: if parsing an error into an event fails we should not hide the
// TODO: original error
/// A WebSocket connection that reconnects upon failure.
class WebSocket {
/// Creates a new websocket
/// To connect the WS call [connect]
WebSocket({
@required this.baseUrl,
this.user,
this.connectParams,
this.connectPayload,
this.handler,
this.logger,
this.connectFunc,
this.reconnectionMonitorInterval = 1,
this.healthCheckInterval = 20,
this.reconnectionMonitorTimeout = 40,
}) {
final qs = Map<String, String>.from(connectParams);
final data = Map<String, dynamic>.from(connectPayload);
data['user_details'] = user.toJson();
qs['json'] = json.encode(data);
if (baseUrl.startsWith('https')) {
_path = baseUrl.replaceFirst('https://', '');
_path = Uri.https(_path, 'connect', qs)
.toString()
.replaceFirst('https', 'wss');
} else if (baseUrl.startsWith('http')) {
_path = baseUrl.replaceFirst('http://', '');
_path =
Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws');
} else {
_path = Uri.https(baseUrl, 'connect', qs)
.toString()
.replaceFirst('https', 'wss');
}
}
/// WS base url
final String baseUrl;
/// User performing the WS connection
final User user;
/// Querystring connection parameters
final Map<String, String> connectParams;
/// WS connection payload
final Map<String, dynamic> connectPayload;
/// Functions that will be called every time a new event is received from the
/// connection
final EventHandler handler;
/// A WS specific logger instance
final Logger logger;
/// Connection function
/// Used only for testing purpose
@visibleForTesting
final ConnectWebSocket connectFunc;
/// Interval of the reconnection monitor timer
/// This checks that it received a new event in the last
/// [reconnectionMonitorTimeout] seconds, otherwise it considers the
/// connection unhealthy and reconnects the WS
final int reconnectionMonitorInterval;
/// Interval of the health event sending timer
/// This sends a health event every [healthCheckInterval] seconds in order to
/// make the server aware that the client is still listening
final int healthCheckInterval;
/// The timeout that uses the reconnection monitor timer to consider the
/// connection unhealthy
final int reconnectionMonitorTimeout;
final _connectionStatusController =
BehaviorSubject.seeded(ConnectionStatus.disconnected);
set _connectionStatus(ConnectionStatus status) =>
_connectionStatusController.add(status);
/// The current connection status value
ConnectionStatus get connectionStatus => _connectionStatusController.value;
/// This notifies of connection status changes
Stream<ConnectionStatus> get connectionStatusStream =>
_connectionStatusController.stream;
String _path;
int _retryAttempt = 1;
WebSocketChannel _channel;
Timer _healthCheck, _reconnectionMonitor;
DateTime _lastEventAt;
bool _manuallyDisconnected = false;
bool _connecting = false;
bool _reconnecting = false;
Event _decodeEvent(String source) => Event.fromJson(json.decode(source));
Completer<Event> _connectionCompleter = Completer<Event>();
/// Connect the WS using the parameters passed in the constructor
Future<Event> connect() {
_manuallyDisconnected = false;
if (_connecting) {
logger.severe('already connecting');
return null;
}
_connecting = true;
_connectionStatus = ConnectionStatus.connecting;
logger.info('connecting to $_path');
_channel =
connectFunc?.call(_path) ?? WebSocketChannel.connect(Uri.parse(_path));
_channel.stream.listen(
(data) {
final jsonData = json.decode(data);
if (jsonData['error'] != null) {
return _onConnectionError(jsonData['error']);
}
_onData(data);
},
onError: (error, stacktrace) {
_onConnectionError(error, stacktrace);
},
onDone: () {
_onDone();
},
);
return _connectionCompleter.future;
}
void _onDone() {
_connecting = false;
if (_manuallyDisconnected) {
return;
}
logger.info('connection closed | closeCode: ${_channel.closeCode} | '
'closedReason: ${_channel.closeReason}');
if (!_reconnecting) {
_reconnect();
}
}
void _onData(data) {
if (_manuallyDisconnected) {
return;
}
final event = _decodeEvent(data);
logger.info('received new event: $data');
if (_lastEventAt == null) {
logger.info('connection estabilished');
_connecting = false;
_reconnecting = false;
_lastEventAt = DateTime.now();
_connectionStatus = ConnectionStatus.connected;
_retryAttempt = 1;
if (!_connectionCompleter.isCompleted) {
_connectionCompleter.complete(event);
}
_startReconnectionMonitor();
_startHealthCheck();
}
handler(event);
_lastEventAt = DateTime.now();
}
Future<void> _onConnectionError(error, [stacktrace]) async {
logger..severe('error connecting')..severe(error);
if (stacktrace != null) {
logger.severe(stacktrace);
}
_connecting = false;
if (!_reconnecting) {
_connectionStatus = ConnectionStatus.disconnected;
}
if (!_connectionCompleter.isCompleted) {
_cancelTimers();
_connectionCompleter.completeError(error, stacktrace);
} else if (!_reconnecting) {
return _reconnect();
}
}
void _reconnectionTimer(_) {
final now = DateTime.now();
if (_lastEventAt != null &&
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
_channel.sink.close();
}
}
void _startReconnectionMonitor() {
_reconnectionMonitor = Timer.periodic(
Duration(seconds: reconnectionMonitorInterval),
_reconnectionTimer,
);
_reconnectionTimer(_reconnectionMonitor);
}
void _reconnectTimer() async {
if (!_reconnecting) {
return;
}
if (_connecting) {
logger.info('already connecting');
return;
}
logger.info('reconnecting..');
_cancelTimers();
try {
await connect();
} catch (e) {
logger.log(Level.SEVERE, e.toString());
}
await Future.delayed(
Duration(seconds: min(_retryAttempt * 5, 25)),
() {
_reconnectTimer();
_retryAttempt++;
},
);
}
Future<void> _reconnect() async {
logger.info('reconnect');
if (!_reconnecting) {
_reconnecting = true;
_connectionStatus = ConnectionStatus.connecting;
}
_reconnectTimer();
}
void _cancelTimers() {
_lastEventAt = null;
if (_healthCheck != null) {
_healthCheck.cancel();
}
if (_reconnectionMonitor != null) {
_reconnectionMonitor.cancel();
}
}
void _healthCheckTimer(_) {
logger.info('sending health.check');
_channel.sink.add("{'type': 'health.check'}");
}
void _startHealthCheck() {
logger.info('start health check monitor');
_healthCheck = Timer.periodic(
Duration(seconds: healthCheckInterval),
_healthCheckTimer,
);
_healthCheckTimer(_healthCheck);
}
/// Disconnects the WS and releases eventual resources
Future<void> disconnect() async {
_connecting = false;
if (!_connectionCompleter.isCompleted) {
_connectionCompleter.complete();
}
if (_manuallyDisconnected) {
return;
}
logger.info('disconnecting');
_connectionCompleter = Completer();
_cancelTimers();
_reconnecting = false;
_manuallyDisconnected = true;
_connectionStatus = ConnectionStatus.disconnected;
await _connectionStatusController.close();
return _channel.sink.close();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
import 'package:stream_chat/src/client/client.dart';
import 'package:stream_chat/src/core/error/error.dart';
/// The retry options
/// When sending/updating/deleting a message any temporary error will trigger the retry policy
/// The retry policy exposes 2 methods
/// - shouldRetry: returns a boolean if the request should be retried
/// - retryTimeout: How many milliseconds to wait till the next attempt
///
/// maxRetryAttempts is a hard limit on maximum retry attempts before giving up
class RetryPolicy {
/// Instantiate a new RetryPolicy
RetryPolicy({
required this.shouldRetry,
required this.retryTimeout,
this.maxRetryAttempts = 6,
});
/// Hard limit on maximum retry attempts before giving up, defaults to 6
/// Resets once connection recovers.
final int maxRetryAttempts;
/// This function evaluates if we should retry the failure
final bool Function(
StreamChatClient client,
int attempt,
StreamChatError? error,
) shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout
/// method is called to determine the timeout
final Duration Function(
StreamChatClient client,
int attempt,
StreamChatError? error,
) retryTimeout;
}
@@ -0,0 +1,241 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
class RetryQueue {
/// Instantiate a new RetryQueue object
RetryQueue({
required this.channel,
this.logger,
}) : client = channel.client {
_retryPolicy = client.retryPolicy;
_listenConnectionRecovered();
_listenFailedEvents();
}
/// The channel of this queue
final Channel channel;
/// The client associated with this [channel]
final StreamChatClient client;
/// The logger associated to this queue
final Logger? logger;
late final RetryPolicy _retryPolicy;
final _compositeSubscription = CompositeSubscription();
final _messageQueue = HeapPriorityQueue(_byDate);
bool _isRetrying = false;
void _listenConnectionRecovered() {
client.on(EventType.connectionRecovered).listen((event) {
if (event.online == true) {
_startRetrying();
}
}).addTo(_compositeSubscription);
}
void _listenFailedEvents() {
channel.on().where((event) => event.message != null).listen((event) {
final message = event.message!;
final containsMessage = _messageQueue.containsMessage(message);
if (!containsMessage) return;
if (message.status == MessageSendingStatus.sent) {
logger?.info('Removing sent message from queue : ${message.id}');
_messageQueue.removeMessage(message);
return;
} else {
if ([
MessageSendingStatus.failed_update,
MessageSendingStatus.failed,
MessageSendingStatus.failed_delete,
].contains(message.status)) {
logger?.info('Adding failed message from event : ${event.type}');
add([message]);
}
}
}).addTo(_compositeSubscription);
}
/// Add a list of messages
void add(List<Message> messages) {
if (messages.isEmpty) return;
if (_messageQueue.containsAllMessage(messages)) return;
logger?.info('Adding ${messages.length} messages');
final messageList = _messageQueue.toList();
// we should not add message if already available in the queue
_messageQueue.addAll(messages.where(
(it) => !messageList.any((m) => m.id == it.id),
));
_startRetrying();
}
Future<void> _startRetrying() async {
if (_isRetrying) return;
_isRetrying = true;
logger?.info('Started retrying failed messages');
while (_messageQueue.isNotEmpty) {
logger?.info('${_messageQueue.length} messages remaining in the queue');
final message = _messageQueue.first;
await _runAndRetry(message);
}
_isRetrying = false;
}
Future<void> _runAndRetry(Message message) async {
var attempt = 1;
final maxAttempt = _retryPolicy.maxRetryAttempts;
// early return in case maxAttempt is less than 0
if (attempt > maxAttempt) return;
// ignore: literal_only_boolean_expressions
while (true) {
try {
logger?.info('Message (${message.id}) retry attempt $attempt');
await _retryMessage(message);
logger?.info('Message (${message.id}) sent successfully');
_messageQueue.removeMessage(message);
break;
} on StreamChatError catch (e) {
// retry logic
final maxAttempt = _retryPolicy.maxRetryAttempts;
if (attempt < maxAttempt) {
final shouldRetry = _retryPolicy.shouldRetry(client, attempt, e);
if (shouldRetry) {
final timeout = _retryPolicy.retryTimeout(client, attempt, e);
// temporary failure, continue
logger?.info(
'API call failed (attempt $attempt), '
'retrying in ${timeout.inSeconds} seconds. Error was $e',
);
await Future.delayed(timeout);
attempt += 1;
} else {
logger?.info(
'API call failed (attempt $attempt). '
'Giving up for now, will retry when connection recovers. '
'Error was $e',
);
_sendFailedEvent(message);
break;
}
} else {
logger?.info(
'API call failed (attempt $attempt). '
'Exceeds maxRetryAttempt : $maxAttempt '
'Giving up for now, will retry when connection recovers. '
'Error was $e',
);
_sendFailedEvent(message);
break;
}
} catch (e) {
logger?.info(
'API call failed due to unknown error (attempt $attempt). '
'Giving up for now, will retry when connection recovers. '
'Error was $e',
);
_sendFailedEvent(message);
break;
}
}
}
void _sendFailedEvent(Message message) {
final newStatus = message.status == MessageSendingStatus.sending
? MessageSendingStatus.failed
: message.status == MessageSendingStatus.updating
? MessageSendingStatus.failed_update
: MessageSendingStatus.failed_delete;
channel.state?.addMessage(message.copyWith(status: newStatus));
}
Future<void> _retryMessage(Message message) async {
if (message.status == MessageSendingStatus.failed_update ||
message.status == MessageSendingStatus.updating) {
await channel.updateMessage(message);
} else if (message.status == MessageSendingStatus.failed ||
message.status == MessageSendingStatus.sending) {
await channel.sendMessage(message);
} else if (message.status == MessageSendingStatus.failed_delete ||
message.status == MessageSendingStatus.deleting) {
await channel.deleteMessage(message);
}
}
/// Whether our [_messageQueue] has messages or not
bool get hasMessages => _messageQueue.isNotEmpty;
/// Call this method to dispose this object
void dispose() {
_messageQueue.clear();
_compositeSubscription.dispose();
}
static int _byDate(Message m1, Message m2) {
final date1 = _getMessageDate(m1);
final date2 = _getMessageDate(m2);
if (date1 == null || date2 == null) {
return 0;
}
return date1.compareTo(date2);
}
static DateTime? _getMessageDate(Message m1) {
switch (m1.status) {
case MessageSendingStatus.failed_delete:
case MessageSendingStatus.deleting:
return m1.deletedAt;
case MessageSendingStatus.failed:
case MessageSendingStatus.sending:
return m1.createdAt;
case MessageSendingStatus.failed_update:
case MessageSendingStatus.updating:
return m1.updatedAt;
default:
return null;
}
}
}
extension _MessageHeapPriorityQueue on HeapPriorityQueue<Message> {
void removeMessage(Message message) {
final list = toUnorderedList();
final index = list.indexWhere((it) => it.id == message.id);
if (index == -1) return;
final element = list[index];
remove(element);
}
bool containsMessage(Message message) {
final list = toUnorderedList();
final index = list.indexWhere((it) => it.id == message.id);
if (index == -1) return false;
return true;
}
bool containsAllMessage(List<Message> messages) {
if (isEmpty) return false;
final list = toUnorderedList();
final messageIds = messages.map((it) => it.id);
return list.every((it) => messageIds.contains(it.id));
}
}
@@ -1,8 +1,7 @@
import 'package:dio/dio.dart';
import 'package:stream_chat/src/api/responses.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/models/attachment_file.dart';
import 'package:stream_chat/src/extensions/string_extension.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
/// Class responsible for uploading images and files to a given channel
abstract class AttachmentFileUploader {
@@ -15,8 +14,8 @@ abstract class AttachmentFileUploader {
AttachmentFile image,
String channelId,
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
});
/// Uploads a [file] to the given channel.
@@ -28,8 +27,8 @@ abstract class AttachmentFileUploader {
AttachmentFile file,
String channelId,
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
});
/// Deletes a image using its [url] from the given channel.
@@ -40,7 +39,7 @@ abstract class AttachmentFileUploader {
String url,
String channelId,
String channelType, {
CancelToken cancelToken,
CancelToken? cancelToken,
});
/// Deletes a file using its [url] from the given channel.
@@ -51,7 +50,7 @@ abstract class AttachmentFileUploader {
String url,
String channelId,
String channelType, {
CancelToken cancelToken,
CancelToken? cancelToken,
});
}
@@ -60,43 +59,24 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
/// Creates a new [StreamAttachmentFileUploader] instance.
const StreamAttachmentFileUploader(this._client);
final StreamChatClient _client;
final StreamHttpClient _client;
@override
Future<SendImageResponse> sendImage(
AttachmentFile file,
String channelId,
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
}) async {
final filename = file.path?.split('/')?.last ?? file.name;
final mimeType = filename.mimeType;
MultipartFile multiPartFile;
if (file.path != null) {
multiPartFile = await MultipartFile.fromFile(
file.path,
filename: filename,
contentType: mimeType,
);
} else if (file.bytes != null) {
multiPartFile = MultipartFile.fromBytes(
file.bytes,
filename: filename,
contentType: mimeType,
);
}
final response = await _client.post(
final multiPartFile = await file.toMultipartFile();
final response = await _client.postFile(
'/channels/$channelType/$channelId/image',
data: FormData.fromMap({
'file': multiPartFile,
}),
multiPartFile,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
return _client.decode(response.data, SendImageResponse.fromJson);
return SendImageResponse.fromJson(response.data);
}
@override
@@ -104,36 +84,17 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
AttachmentFile file,
String channelId,
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
ProgressCallback? onSendProgress,
CancelToken? cancelToken,
}) async {
final filename = file.path?.split('/')?.last ?? file.name;
final mimeType = filename.mimeType;
MultipartFile multiPartFile;
if (file.path != null) {
multiPartFile = await MultipartFile.fromFile(
file.path,
filename: filename,
contentType: mimeType,
);
} else if (file.bytes != null) {
multiPartFile = MultipartFile.fromBytes(
file.bytes,
filename: filename,
contentType: mimeType,
);
}
final response = await _client.post(
final multiPartFile = await file.toMultipartFile();
final response = await _client.postFile(
'/channels/$channelType/$channelId/file',
data: FormData.fromMap({
'file': multiPartFile,
}),
multiPartFile,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
return _client.decode(response.data, SendFileResponse.fromJson);
return SendFileResponse.fromJson(response.data);
}
@override
@@ -141,14 +102,14 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
String url,
String channelId,
String channelType, {
CancelToken cancelToken,
CancelToken? cancelToken,
}) async {
final response = await _client.delete(
'/channels/$channelType/$channelId/image',
queryParameters: {'url': url},
cancelToken: cancelToken,
);
return _client.decode(response.data, EmptyResponse.fromJson);
return EmptyResponse.fromJson(response.data);
}
@override
@@ -156,13 +117,13 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
String url,
String channelId,
String channelType, {
CancelToken cancelToken,
CancelToken? cancelToken,
}) async {
final response = await _client.delete(
'/channels/$channelType/$channelId/file',
queryParameters: {'url': url},
cancelToken: cancelToken,
);
return _client.decode(response.data, EmptyResponse.fromJson);
return EmptyResponse.fromJson(response.data);
}
}
@@ -0,0 +1,295 @@
import 'dart:convert';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/message.dart';
/// Defines the api dedicated to channel operations
class ChannelApi {
/// Initialize a new channel api
ChannelApi(this._client);
final StreamHttpClient _client;
String _getChannelUrl(String channelId, String channelType) =>
'/channels/$channelType/$channelId';
/// Query the API, get messages, members or other channel fields
Future<ChannelState> queryChannel(
String channelType, {
bool state = true,
bool watch = false,
bool presence = false,
String? channelId,
Map<String, Object?>? channelData,
PaginationParams? messagesPagination,
PaginationParams? membersPagination,
PaginationParams? watchersPagination,
}) async {
var channelPath = '/channels/$channelType';
if (channelId != null) channelPath = '$channelPath/$channelId';
final response = await _client.post(
'$channelPath/query',
data: {
'state': state,
'watch': watch,
'presence': presence,
if (channelData != null) 'data': channelData,
if (messagesPagination != null) 'messages': messagesPagination,
if (membersPagination != null) 'members': membersPagination,
if (watchersPagination != null) 'watchers': watchersPagination,
},
);
return ChannelState.fromJson(response.data);
}
/// Requests channels with a given query from the API.
Future<QueryChannelsResponse> queryChannels({
Filter? filter,
List<SortOption<ChannelModel>>? sort,
int? memberLimit,
int? messageLimit,
bool state = true,
bool watch = true,
bool presence = false,
PaginationParams paginationParams = const PaginationParams(),
}) async {
final response = await _client.get(
'/channels',
queryParameters: {
'payload': jsonEncode({
// default options
'state': state,
'watch': watch,
'presence': presence,
// passed options
if (sort != null) 'sort': sort,
if (filter != null) 'filter_conditions': filter,
if (memberLimit != null) 'member_limit': memberLimit,
if (messageLimit != null) 'message_limit': messageLimit,
// pagination
...paginationParams.toJson()
}),
},
);
return QueryChannelsResponse.fromJson(response.data);
}
/// Mark all channels for this user as read
Future<EmptyResponse> markAllRead() async {
final response = await _client.post('channels/read');
return EmptyResponse.fromJson(response.data);
}
/// Replaces the [channelId] of type [ChannelType] data with [data]
Future<UpdateChannelResponse> updateChannel(
String channelId,
String channelType,
Map<String, Object?> data, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'data': data,
if (message != null)
'message': message.copyWith(updatedAt: DateTime.now()),
},
);
return UpdateChannelResponse.fromJson(response.data);
}
/// Updates the [channelId] of type [ChannelType] data with [data]
Future<PartialUpdateChannelResponse> updateChannelPartial(
String channelId,
String channelType, {
Map<String, Object?>? set,
List<String>? unset,
}) async {
final response = await _client.patch(
_getChannelUrl(channelId, channelType),
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
},
);
return PartialUpdateChannelResponse.fromJson(response.data);
}
/// Accept invitation to the channel
Future<AcceptInviteResponse> acceptChannelInvite(
String channelId,
String channelType, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'accept_invite': true,
'message': message,
},
);
return AcceptInviteResponse.fromJson(response.data);
}
/// Reject invitation to the channel
Future<RejectInviteResponse> rejectChannelInvite(
String channelId,
String channelType, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'reject_invite': true,
'message': message,
},
);
return RejectInviteResponse.fromJson(response.data);
}
/// Invite members to the channel
Future<InviteMembersResponse> inviteChannelMembers(
String channelId,
String channelType,
List<String> memberIds, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'invites': memberIds,
'message': message,
},
);
return InviteMembersResponse.fromJson(response.data);
}
/// Add members to the channel
Future<AddMembersResponse> addMembers(
String channelId,
String channelType,
List<String> memberIds, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'add_members': memberIds,
'message': message,
},
);
return AddMembersResponse.fromJson(response.data);
}
/// Remove members from the channel
Future<RemoveMembersResponse> removeMembers(
String channelId,
String channelType,
List<String> memberIds, {
Message? message,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'remove_members': memberIds,
'message': message,
},
);
return RemoveMembersResponse.fromJson(response.data);
}
/// Send an event on this channel
Future<EmptyResponse> sendEvent(
String channelId,
String channelType,
Event event,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/event',
data: {'event': event},
);
return EmptyResponse.fromJson(response.data);
}
/// Delete this channel. Messages are permanently removed.
Future<EmptyResponse> deleteChannel(
String channelId,
String channelType,
) async {
final response = await _client.delete(
_getChannelUrl(channelId, channelType),
);
return EmptyResponse.fromJson(response.data);
}
/// Removes all messages from the channel
Future<EmptyResponse> truncateChannel(
String channelId,
String channelType,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/truncate',
);
return EmptyResponse.fromJson(response.data);
}
/// Hides the channel from [StreamChatClient.queryChannels] for the user
/// until a message is added If [clearHistory] is set to true - all messages
/// will be removed for the user
Future<EmptyResponse> hideChannel(
String channelId,
String channelType, {
bool clearHistory = false,
}) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/hide',
data: {'clear_history': clearHistory},
);
return EmptyResponse.fromJson(response.data);
}
/// Removes the hidden status for the channel
Future<EmptyResponse> showChannel(
String channelId,
String channelType,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/show',
);
return EmptyResponse.fromJson(response.data);
}
/// Mark [channelId] of type [channelType] all messages as read
/// Optionally provide a [messageId] if you want to mark a
/// particular message as read
Future<EmptyResponse> markRead(
String channelId,
String channelType, {
String? messageId,
}) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/read',
data: {if (messageId != null) 'message_id': messageId},
);
return EmptyResponse.fromJson(response.data);
}
/// Stop watching the channel
Future<EmptyResponse> stopWatching(
String channelId,
String channelType,
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/stop-watching',
);
return EmptyResponse.fromJson(response.data);
}
}
@@ -0,0 +1,60 @@
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
/// Provider used to send push notifications.
enum PushProvider {
/// Send notifications using Google's Firebase Cloud Messaging
firebase,
/// Send notifications using Apple's Push Notification service
apn
}
/// Helper extension for [PushProvider]
extension PushProviderX on PushProvider {
/// Returns the string notion for [PushProvider].
String get name => {
PushProvider.apn: 'apn',
PushProvider.firebase: 'firebase',
}[this]!;
}
/// Defines the api dedicated to device operations
class DeviceApi {
/// Initialize a new device api
DeviceApi(this._client);
final StreamHttpClient _client;
/// Add a device for Push Notifications.
Future<EmptyResponse> addDevice(
String deviceId,
PushProvider pushProvider,
) async {
final response = await _client.post(
'/devices',
data: {
'id': deviceId,
'push_provider': pushProvider.name,
},
);
return EmptyResponse.fromJson(response.data);
}
/// Gets a list of user devices.
Future<ListDevicesResponse> getDevices() async {
final response = await _client.get('/devices');
return ListDevicesResponse.fromJson(response.data);
}
/// Remove a user's device.
Future<EmptyResponse> removeDevice(
String deviceId,
) async {
final response = await _client.delete(
'/devices',
queryParameters: {'id': deviceId},
);
return EmptyResponse.fromJson(response.data);
}
}
@@ -0,0 +1,95 @@
import 'dart:convert';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/member.dart';
/// Defines the api dedicated to general operations
class GeneralApi {
/// Initialize a new general api
GeneralApi(this._client);
final StreamHttpClient _client;
/// Get all the missed events
Future<SyncResponse> sync(
List<String> cids,
DateTime lastSyncAt,
) async {
final response = await _client.post(
'/sync',
data: {
'channel_cids': cids,
'last_sync_at': lastSyncAt.toUtc().toIso8601String(),
},
);
return SyncResponse.fromJson(response.data);
}
/// A message search.
Future<SearchMessagesResponse> searchMessages(
Filter filter, {
String? query,
List<SortOption>? sort,
PaginationParams? pagination,
Filter? messageFilters,
}) async {
assert(() {
if (query == null && messageFilters == null) {
throw ArgumentError('Provide at least `query` or `messageFilters`');
}
if (query != null && messageFilters != null) {
throw ArgumentError(
"Can't provide both `query` and `messageFilters` at the same time",
);
}
return true;
}(), 'Check incoming params.');
final response = await _client.get(
'/search',
queryParameters: {
'payload': jsonEncode({
'filter_conditions': filter,
if (sort != null) 'sort': sort,
if (query != null) 'query': query,
if (messageFilters != null)
'message_filter_conditions': messageFilters,
if (pagination != null) ...pagination.toJson(),
}),
},
);
return SearchMessagesResponse.fromJson(response.data);
}
/// Query channel members
Future<QueryMembersResponse> queryMembers(
String channelType, {
Filter? filter,
String? channelId,
List<Member>? members,
List<SortOption>? sort,
PaginationParams? pagination,
}) async {
final response = await _client.get(
'/members',
queryParameters: {
'payload': jsonEncode({
'type': channelType,
'filter_conditions': filter ?? {},
if (channelId != null)
'id': channelId
else if (members != null)
'members': members,
if (sort != null) 'sort': sort,
if (pagination != null) ...pagination.toJson(),
}),
},
);
return QueryMembersResponse.fromJson(response.data);
}
}
@@ -0,0 +1,20 @@
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/user.dart';
/// Defines the api dedicated to guest users operations
class GuestApi {
/// Initialize a new guest api
GuestApi(this._client);
final StreamHttpClient _client;
/// Returns the information about guest user
Future<ConnectGuestUserResponse> getGuestUser(User user) async {
final response = await _client.post(
'/guest',
data: {'user': user},
);
return ConnectGuestUserResponse.fromJson(response.data);
}
}
@@ -0,0 +1,182 @@
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/message.dart';
/// Defines the api dedicated to messages operations
class MessageApi {
/// Initialize a new message api
MessageApi(this._client);
final StreamHttpClient _client;
/// Sends the [message] to the given [channelId] of given [channelType]
Future<SendMessageResponse> sendMessage(
String channelId,
String channelType,
Message message, {
bool skipPush = false,
}) async {
final response = await _client.post(
'/channels/$channelType/$channelId/message',
data: {
'message': message,
'skip_push': skipPush,
},
);
return SendMessageResponse.fromJson(response.data);
}
/// Retrieves a list of messages by [messageIDs]
/// from the given [channelId] of type [channelType]
Future<GetMessagesByIdResponse> getMessagesById(
String channelId,
String channelType,
List<String> messageIDs,
) async {
final response = await _client.get(
'/channels/$channelType/$channelId/messages',
queryParameters: {'ids': messageIDs.join(',')},
);
return GetMessagesByIdResponse.fromJson(response.data);
}
/// Get a message by [messageId]
Future<GetMessageResponse> getMessage(String messageId) async {
final response = await _client.get(
'/messages/$messageId',
);
return GetMessageResponse.fromJson(response.data);
}
/// Updates the given [message]
Future<UpdateMessageResponse> updateMessage(
Message message,
) async {
final response = await _client.post(
'/messages/${message.id}',
data: {'message': message},
);
return UpdateMessageResponse.fromJson(response.data);
}
/// Partially update the given [messageId]
/// Use [set] to define values to be set
/// Use [unset] to define values to be unset
Future<UpdateMessageResponse> partialUpdateMessage(
String messageId, {
Map<String, Object?>? set,
List<String>? unset,
}) async {
final response = await _client.put(
'/messages/$messageId',
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
},
);
return UpdateMessageResponse.fromJson(response.data);
}
/// Deletes the given [messageId]
Future<EmptyResponse> deleteMessage(
String messageId,
) async {
final response = await _client.delete(
'/messages/$messageId',
);
return EmptyResponse.fromJson(response.data);
}
/// Send action for a specific [messageId]
/// of the given [channelId] of given [channelType]
Future<SendActionResponse> sendAction(
String channelId,
String channelType,
String messageId,
Map<String, Object?> formData,
) async {
final response = await _client.post(
'/messages/$messageId/action',
data: {
'id': channelId,
'type': channelType,
'form_data': formData,
'message_id': messageId,
},
);
return SendActionResponse.fromJson(response.data);
}
/// Send a [reactionType] for this [messageId]
/// Set [enforceUnique] to true to remove the existing user reaction
Future<SendReactionResponse> sendReaction(
String messageId,
String reactionType, {
Map<String, Object?> extraData = const {},
bool enforceUnique = false,
}) async {
final reaction = Map<String, Object?>.from(extraData)
..addAll({'type': reactionType});
final response = await _client.post(
'/messages/$messageId/reaction',
data: {
'reaction': reaction,
'enforce_unique': enforceUnique,
},
);
return SendReactionResponse.fromJson(response.data);
}
/// Delete a [reactionType] from this [messageId]
Future<EmptyResponse> deleteReaction(
String messageId,
String reactionType,
) async {
final response = await _client.delete(
'/messages/$messageId/reaction/$reactionType',
);
return EmptyResponse.fromJson(response.data);
}
/// Get all the reactions for a [messageId]
Future<QueryReactionsResponse> getReactions(
String messageId, {
PaginationParams? pagination,
}) async {
final response = await _client.get(
'/messages/$messageId/reactions',
queryParameters: {
if (pagination != null) ...pagination.toJson(),
},
);
return QueryReactionsResponse.fromJson(response.data);
}
/// Translates the [messageId] in provided [language]
Future<TranslateMessageResponse> translateMessage(
String messageId,
String language,
) async {
final response = await _client.post(
'/messages/$messageId/translate',
data: {'language': language},
);
return TranslateMessageResponse.fromJson(response.data);
}
/// Lists all the message replies for the [parentId]
Future<QueryRepliesResponse> getReplies(
String parentId, {
PaginationParams? options,
}) async {
final response = await _client.get(
'/messages/$parentId/replies',
queryParameters: {
if (options != null) ...options.toJson(),
},
);
return QueryRepliesResponse.fromJson(response.data);
}
}
@@ -0,0 +1,128 @@
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
/// Defines the api dedicated to moderation operations
class ModerationApi {
/// Initialize a new moderation api
ModerationApi(this._client);
final StreamHttpClient _client;
/// Mutes a user
Future<EmptyResponse> muteUser(String userId) async {
final response = await _client.post(
'/moderation/mute',
data: {'target_id': userId},
);
return EmptyResponse.fromJson(response.data);
}
/// Unmutes a user
Future<EmptyResponse> unmuteUser(String userId) async {
final response = await _client.post(
'/moderation/unmute',
data: {'target_id': userId},
);
return EmptyResponse.fromJson(response.data);
}
/// Mutes the channel
Future<EmptyResponse> muteChannel(
String channelCid, {
Duration? expiration,
}) async {
final response = await _client.post(
'/moderation/mute/channel',
data: {
'channel_cid': channelCid,
if (expiration != null) 'expiration': expiration.inMilliseconds,
},
);
return EmptyResponse.fromJson(response.data);
}
/// Unmutes the channel
Future<EmptyResponse> unmuteChannel(
String channelCid,
) async {
final response = await _client.post(
'/moderation/unmute/channel',
data: {'channel_cid': channelCid},
);
return EmptyResponse.fromJson(response.data);
}
/// Flag a message
Future<EmptyResponse> flagMessage(
String messageId,
) async {
final response = await _client.post(
'/moderation/flag',
data: {'target_message_id': messageId},
);
return EmptyResponse.fromJson(response.data);
}
/// Unflag a message
Future<EmptyResponse> unflagMessage(
String messageId,
) async {
final response = await _client.post(
'/moderation/unflag',
data: {'target_message_id': messageId},
);
return EmptyResponse.fromJson(response.data);
}
/// Flag a user
Future<EmptyResponse> flagUser(
String userId,
) async {
final response = await _client.post(
'/moderation/flag',
data: {'target_user_id': userId},
);
return EmptyResponse.fromJson(response.data);
}
/// Unflag a user
Future<EmptyResponse> unflagUser(
String userId,
) async {
final response = await _client.post(
'/moderation/unflag',
data: {'target_user_id': userId},
);
return EmptyResponse.fromJson(response.data);
}
/// Bans a user from all channels
Future<EmptyResponse> banUser(
String targetUserId, {
Map<String, Object?>? options,
}) async {
final response = await _client.post(
'/moderation/ban',
data: {
'target_user_id': targetUserId,
if (options != null) ...options,
},
);
return EmptyResponse.fromJson(response.data);
}
/// Remove global ban for a user
Future<EmptyResponse> unbanUser(
String targetUserId, {
Map<String, Object?>? options,
}) async {
final response = await _client.delete(
'/moderation/ban',
queryParameters: {
'target_user_id': targetUserId,
if (options != null) ...options,
},
);
return EmptyResponse.fromJson(response.data);
}
}
@@ -1,9 +1,10 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'requests.g.dart';
/// Sorting options
@JsonSerializable(createFactory: false)
@JsonSerializable(includeIfNull: false)
class SortOption<T> {
/// Creates a new SortOption instance
///
@@ -18,6 +19,10 @@ class SortOption<T> {
this.comparator,
});
/// Create a new instance from a json
factory SortOption.fromJson(Map<String, dynamic> json) =>
_$SortOptionFromJson(json);
/// Ascending order
// ignore: constant_identifier_names
static const ASC = 1;
@@ -34,15 +39,15 @@ class SortOption<T> {
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
final Comparator<T>? comparator;
/// Serialize model to json
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
}
/// Pagination options.
@JsonSerializable(createFactory: false, includeIfNull: false)
class PaginationParams {
@JsonSerializable(includeIfNull: false)
class PaginationParams extends Equatable {
/// Creates a new PaginationParams instance
///
/// For example:
@@ -62,6 +67,10 @@ class PaginationParams {
this.lessThanOrEqual,
});
/// Create a new instance from a json
factory PaginationParams.fromJson(Map<String, dynamic> json) =>
_$PaginationParamsFromJson(json);
/// The amount of items requested from the APIs.
final int limit;
@@ -70,31 +79,31 @@ class PaginationParams {
/// Filter on ids greater than the given value.
@JsonKey(name: 'id_gt')
final String greaterThan;
final String? greaterThan;
/// Filter on ids greater than or equal to the given value.
@JsonKey(name: 'id_gte')
final String greaterThanOrEqual;
final String? greaterThanOrEqual;
/// Filter on ids smaller than the given value.
@JsonKey(name: 'id_lt')
final String lessThan;
final String? lessThan;
/// Filter on ids smaller than or equal to the given value.
@JsonKey(name: 'id_lte')
final String lessThanOrEqual;
final String? lessThanOrEqual;
/// Serialize model to json
Map<String, dynamic> toJson() => _$PaginationParamsToJson(this);
/// Creates a copy of [PaginationParams] with specified attributes overridden.
PaginationParams copyWith({
int limit,
int offset,
String greaterThan,
String greaterThanOrEqual,
String lessThan,
String lessThanOrEqual,
int? limit,
int? offset,
String? greaterThan,
String? greaterThanOrEqual,
String? lessThan,
String? lessThanOrEqual,
}) =>
PaginationParams(
limit: limit ?? this.limit,
@@ -106,23 +115,12 @@ class PaginationParams {
);
@override
int get hashCode =>
runtimeType.hashCode ^
limit.hashCode ^
offset.hashCode ^
greaterThan.hashCode ^
greaterThanOrEqual.hashCode ^
lessThan.hashCode ^
lessThanOrEqual.hashCode;
@override
bool operator ==(covariant PaginationParams other) =>
identical(this, other) ||
runtimeType == other.runtimeType &&
limit == other.limit &&
offset == other.offset &&
greaterThan == other.greaterThan &&
greaterThanOrEqual == other.greaterThanOrEqual &&
lessThan == other.lessThan &&
lessThanOrEqual == other.lessThanOrEqual;
List<Object?> get props => [
limit,
offset,
greaterThan,
greaterThanOrEqual,
lessThan,
lessThanOrEqual,
];
}
@@ -6,14 +6,35 @@ part of 'requests.dart';
// JsonSerializableGenerator
// **************************************************************************
SortOption<T> _$SortOptionFromJson<T>(Map<String, dynamic> json) {
return SortOption<T>(
json['field'] as String,
direction: json['direction'] as int,
);
}
Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
<String, dynamic>{
'field': instance.field,
'direction': instance.direction,
};
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) {
return PaginationParams(
limit: json['limit'] as int,
offset: json['offset'] as int,
greaterThan: json['id_gt'] as String?,
greaterThanOrEqual: json['id_gte'] as String?,
lessThan: json['id_lt'] as String?,
lessThanOrEqual: json['id_lte'] as String?,
);
}
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{};
final val = <String, dynamic>{
'limit': instance.limit,
'offset': instance.offset,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
@@ -21,8 +42,6 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
}
}
writeNotNull('limit', instance.limit);
writeNotNull('offset', instance.offset);
writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan);
@@ -1,26 +1,58 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/client.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/device.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';
import 'package:stream_chat/src/client/client.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/device.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/src/core/models/user.dart';
part 'responses.g.dart';
class _BaseResponse {
String duration;
String? duration;
}
/// Model response for [StreamChatClient.resync] api call
/// Model response for [StreamChatNetworkError] data
@JsonSerializable()
class ErrorResponse extends _BaseResponse {
/// The http error code
int? code;
/// The message associated to the error code
String? message;
/// The backend error code
@JsonKey(name: 'StatusCode')
int? statusCode;
/// A detailed message about the error
String? moreInfo;
/// Create a new instance from a json
static ErrorResponse fromJson(Map<String, dynamic> json) =>
_$ErrorResponseFromJson(json);
/// Serialize to json
Map<String, dynamic> toJson() => _$ErrorResponseToJson(this);
@override
String toString() => 'ErrorResponse(code: $code, '
'message: $message, '
'statusCode: $statusCode, '
'moreInfo: $moreInfo)';
}
/// Model response for [StreamChatClient.sync] api call
@JsonSerializable(createToJson: false)
class SyncResponse extends _BaseResponse {
/// The list of events
List<Event> events;
@JsonKey(defaultValue: [])
late List<Event> events;
/// Create a new instance from a json
static SyncResponse fromJson(Map<String, dynamic> json) =>
@@ -31,7 +63,8 @@ class SyncResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class QueryChannelsResponse extends _BaseResponse {
/// List of channels state returned by the query
List<ChannelState> channels;
@JsonKey(defaultValue: [])
late List<ChannelState> channels;
/// Create a new instance from a json
static QueryChannelsResponse fromJson(Map<String, dynamic> json) =>
@@ -41,8 +74,8 @@ class QueryChannelsResponse extends _BaseResponse {
/// Model response for [StreamChatClient.queryChannels] api call
@JsonSerializable(createToJson: false)
class TranslateMessageResponse extends _BaseResponse {
/// List of channels state returned by the query
TranslatedMessage message;
/// Translated message
late TranslatedMessage message;
/// Create a new instance from a json
static TranslateMessageResponse fromJson(Map<String, dynamic> json) =>
@@ -53,7 +86,8 @@ class TranslateMessageResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class QueryMembersResponse extends _BaseResponse {
/// List of channels state returned by the query
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Create a new instance from a json
static QueryMembersResponse fromJson(Map<String, dynamic> json) =>
@@ -64,7 +98,8 @@ class QueryMembersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class QueryUsersResponse extends _BaseResponse {
/// List of users returned by the query
List<User> users;
@JsonKey(defaultValue: [])
late List<User> users;
/// Create a new instance from a json
static QueryUsersResponse fromJson(Map<String, dynamic> json) =>
@@ -75,7 +110,8 @@ class QueryUsersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class QueryReactionsResponse extends _BaseResponse {
/// List of reactions returned by the query
List<Reaction> reactions;
@JsonKey(defaultValue: [])
late List<Reaction> reactions;
/// Create a new instance from a json
static QueryReactionsResponse fromJson(Map<String, dynamic> json) =>
@@ -86,7 +122,8 @@ class QueryReactionsResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class QueryRepliesResponse extends _BaseResponse {
/// List of messages returned by the api call
List<Message> messages;
@JsonKey(defaultValue: [])
late List<Message> messages;
/// Create a new instance from a json
static QueryRepliesResponse fromJson(Map<String, dynamic> json) =>
@@ -97,7 +134,8 @@ class QueryRepliesResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class ListDevicesResponse extends _BaseResponse {
/// List of user devices
List<Device> devices;
@JsonKey(defaultValue: [])
late List<Device> devices;
/// Create a new instance from a json
static ListDevicesResponse fromJson(Map<String, dynamic> json) =>
@@ -108,7 +146,7 @@ class ListDevicesResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SendFileResponse extends _BaseResponse {
/// The url of the uploaded file
String file;
late String file;
/// Create a new instance from a json
static SendFileResponse fromJson(Map<String, dynamic> json) =>
@@ -119,7 +157,7 @@ class SendFileResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SendImageResponse extends _BaseResponse {
/// The url of the uploaded file
String file;
late String file;
/// Create a new instance from a json
static SendImageResponse fromJson(Map<String, dynamic> json) =>
@@ -130,10 +168,10 @@ class SendImageResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SendReactionResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
late Message message;
/// The reaction created by the api call
Reaction reaction;
late Reaction reaction;
/// Create a new instance from a json
static SendReactionResponse fromJson(Map<String, dynamic> json) =>
@@ -144,10 +182,10 @@ class SendReactionResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class ConnectGuestUserResponse extends _BaseResponse {
/// Guest user access token
String accessToken;
late String accessToken;
/// Guest user
User user;
late User user;
/// Create a new instance from a json
static ConnectGuestUserResponse fromJson(Map<String, dynamic> json) =>
@@ -158,7 +196,8 @@ class ConnectGuestUserResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class UpdateUsersResponse extends _BaseResponse {
/// Updated users
Map<String, User> users;
@JsonKey(defaultValue: {})
late Map<String, User> users;
/// Create a new instance from a json
static UpdateUsersResponse fromJson(Map<String, dynamic> json) =>
@@ -169,7 +208,7 @@ class UpdateUsersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class UpdateMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
late Message message;
/// Create a new instance from a json
static UpdateMessageResponse fromJson(Map<String, dynamic> json) =>
@@ -180,7 +219,7 @@ class UpdateMessageResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SendMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
late Message message;
/// Create a new instance from a json
static SendMessageResponse fromJson(Map<String, dynamic> json) =>
@@ -191,17 +230,17 @@ class SendMessageResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class GetMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
late Message message;
/// Channel of the message
ChannelModel channel;
ChannelModel? channel;
/// Create a new instance from a json
static GetMessageResponse fromJson(Map<String, dynamic> json) {
final res = _$GetMessageResponseFromJson(json);
final jsonChannel = res.message?.extraData?.remove('channel');
final jsonChannel = res.message.extraData.remove('channel');
if (jsonChannel != null) {
res.channel = ChannelModel.fromJson(jsonChannel);
res.channel = ChannelModel.fromJson(jsonChannel as Map<String, dynamic>);
}
return res;
}
@@ -211,7 +250,8 @@ class GetMessageResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SearchMessagesResponse extends _BaseResponse {
/// List of messages returned by the api call
List<GetMessageResponse> results;
@JsonKey(defaultValue: [])
late List<GetMessageResponse> results;
/// Create a new instance from a json
static SearchMessagesResponse fromJson(Map<String, dynamic> json) =>
@@ -222,7 +262,8 @@ class SearchMessagesResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class GetMessagesByIdResponse extends _BaseResponse {
/// Message returned by the api call
List<Message> messages;
@JsonKey(defaultValue: [])
late List<Message> messages;
/// Create a new instance from a json
static GetMessagesByIdResponse fromJson(Map<String, dynamic> json) =>
@@ -233,13 +274,13 @@ class GetMessagesByIdResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class UpdateChannelResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
List<Member>? members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static UpdateChannelResponse fromJson(Map<String, dynamic> json) =>
@@ -250,10 +291,10 @@ class UpdateChannelResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class PartialUpdateChannelResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
List<Member>? members;
/// Create a new instance from a json
static PartialUpdateChannelResponse fromJson(Map<String, dynamic> json) =>
@@ -264,13 +305,14 @@ class PartialUpdateChannelResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class InviteMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static InviteMembersResponse fromJson(Map<String, dynamic> json) =>
@@ -281,13 +323,14 @@ class InviteMembersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class RemoveMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static RemoveMembersResponse fromJson(Map<String, dynamic> json) =>
@@ -298,7 +341,7 @@ class RemoveMembersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class SendActionResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static SendActionResponse fromJson(Map<String, dynamic> json) =>
@@ -309,13 +352,14 @@ class SendActionResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class AddMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static AddMembersResponse fromJson(Map<String, dynamic> json) =>
@@ -326,13 +370,14 @@ class AddMembersResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class AcceptInviteResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static AcceptInviteResponse fromJson(Map<String, dynamic> json) =>
@@ -343,13 +388,14 @@ class AcceptInviteResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class RejectInviteResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Message returned by the api call
Message message;
Message? message;
/// Create a new instance from a json
static RejectInviteResponse fromJson(Map<String, dynamic> json) =>
@@ -368,19 +414,23 @@ class EmptyResponse extends _BaseResponse {
@JsonSerializable(createToJson: false)
class ChannelStateResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
late ChannelModel channel;
/// List of messages returned by the api call
List<Message> messages;
@JsonKey(defaultValue: [])
late List<Message> messages;
/// Channel members
List<Member> members;
@JsonKey(defaultValue: [])
late List<Member> members;
/// Number of users watching the channel
int watcherCount;
@JsonKey(defaultValue: 0)
late int watcherCount;
/// List of read states
List<Read> read;
@JsonKey(defaultValue: [])
late List<Read> read;
/// Create a new instance from a json
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
@@ -0,0 +1,297 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'responses.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ErrorResponse _$ErrorResponseFromJson(Map<String, dynamic> json) {
return ErrorResponse()
..duration = json['duration'] as String?
..code = json['code'] as int?
..message = json['message'] as String?
..statusCode = json['StatusCode'] as int?
..moreInfo = json['more_info'] as String?;
}
Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
<String, dynamic>{
'duration': instance.duration,
'code': instance.code,
'message': instance.message,
'StatusCode': instance.statusCode,
'more_info': instance.moreInfo,
};
SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) {
return SyncResponse()
..duration = json['duration'] as String?
..events = (json['events'] as List<dynamic>?)
?.map((e) => Event.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
QueryChannelsResponse _$QueryChannelsResponseFromJson(
Map<String, dynamic> json) {
return QueryChannelsResponse()
..duration = json['duration'] as String?
..channels = (json['channels'] as List<dynamic>?)
?.map((e) => ChannelState.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
TranslateMessageResponse _$TranslateMessageResponseFromJson(
Map<String, dynamic> json) {
return TranslateMessageResponse()
..duration = json['duration'] as String?
..message =
TranslatedMessage.fromJson(json['message'] as Map<String, dynamic>);
}
QueryMembersResponse _$QueryMembersResponseFromJson(Map<String, dynamic> json) {
return QueryMembersResponse()
..duration = json['duration'] as String?
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
QueryUsersResponse _$QueryUsersResponseFromJson(Map<String, dynamic> json) {
return QueryUsersResponse()
..duration = json['duration'] as String?
..users = (json['users'] as List<dynamic>?)
?.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
QueryReactionsResponse _$QueryReactionsResponseFromJson(
Map<String, dynamic> json) {
return QueryReactionsResponse()
..duration = json['duration'] as String?
..reactions = (json['reactions'] as List<dynamic>?)
?.map((e) => Reaction.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
QueryRepliesResponse _$QueryRepliesResponseFromJson(Map<String, dynamic> json) {
return QueryRepliesResponse()
..duration = json['duration'] as String?
..messages = (json['messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
ListDevicesResponse _$ListDevicesResponseFromJson(Map<String, dynamic> json) {
return ListDevicesResponse()
..duration = json['duration'] as String?
..devices = (json['devices'] as List<dynamic>?)
?.map((e) => Device.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
SendFileResponse _$SendFileResponseFromJson(Map<String, dynamic> json) {
return SendFileResponse()
..duration = json['duration'] as String?
..file = json['file'] as String;
}
SendImageResponse _$SendImageResponseFromJson(Map<String, dynamic> json) {
return SendImageResponse()
..duration = json['duration'] as String?
..file = json['file'] as String;
}
SendReactionResponse _$SendReactionResponseFromJson(Map<String, dynamic> json) {
return SendReactionResponse()
..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>)
..reaction = Reaction.fromJson(json['reaction'] as Map<String, dynamic>);
}
ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson(
Map<String, dynamic> json) {
return ConnectGuestUserResponse()
..duration = json['duration'] as String?
..accessToken = json['access_token'] as String
..user = User.fromJson(json['user'] as Map<String, dynamic>);
}
UpdateUsersResponse _$UpdateUsersResponseFromJson(Map<String, dynamic> json) {
return UpdateUsersResponse()
..duration = json['duration'] as String?
..users = (json['users'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, User.fromJson(e as Map<String, dynamic>)),
) ??
{};
}
UpdateMessageResponse _$UpdateMessageResponseFromJson(
Map<String, dynamic> json) {
return UpdateMessageResponse()
..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>);
}
SendMessageResponse _$SendMessageResponseFromJson(Map<String, dynamic> json) {
return SendMessageResponse()
..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>);
}
GetMessageResponse _$GetMessageResponseFromJson(Map<String, dynamic> json) {
return GetMessageResponse()
..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>)
..channel = json['channel'] == null
? null
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>);
}
SearchMessagesResponse _$SearchMessagesResponseFromJson(
Map<String, dynamic> json) {
return SearchMessagesResponse()
..duration = json['duration'] as String?
..results = (json['results'] as List<dynamic>?)
?.map((e) => GetMessageResponse.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(
Map<String, dynamic> json) {
return GetMessagesByIdResponse()
..duration = json['duration'] as String?
..messages = (json['messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
UpdateChannelResponse _$UpdateChannelResponseFromJson(
Map<String, dynamic> json) {
return UpdateChannelResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList()
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson(
Map<String, dynamic> json) {
return PartialUpdateChannelResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList();
}
InviteMembersResponse _$InviteMembersResponseFromJson(
Map<String, dynamic> json) {
return InviteMembersResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
RemoveMembersResponse _$RemoveMembersResponseFromJson(
Map<String, dynamic> json) {
return RemoveMembersResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
SendActionResponse _$SendActionResponseFromJson(Map<String, dynamic> json) {
return SendActionResponse()
..duration = json['duration'] as String?
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
AddMembersResponse _$AddMembersResponseFromJson(Map<String, dynamic> json) {
return AddMembersResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
AcceptInviteResponse _$AcceptInviteResponseFromJson(Map<String, dynamic> json) {
return AcceptInviteResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
RejectInviteResponse _$RejectInviteResponseFromJson(Map<String, dynamic> json) {
return RejectInviteResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..message = json['message'] == null
? null
: Message.fromJson(json['message'] as Map<String, dynamic>);
}
EmptyResponse _$EmptyResponseFromJson(Map<String, dynamic> json) {
return EmptyResponse()..duration = json['duration'] as String?;
}
ChannelStateResponse _$ChannelStateResponseFromJson(Map<String, dynamic> json) {
return ChannelStateResponse()
..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..messages = (json['messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..watcherCount = json['watcher_count'] as int? ?? 0
..read = (json['read'] as List<dynamic>?)
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
}
@@ -0,0 +1,79 @@
import 'package:logging/logging.dart';
import 'package:stream_chat/src/core/api/attachment_file_uploader.dart';
import 'package:stream_chat/src/core/api/channel_api.dart';
import 'package:stream_chat/src/core/api/device_api.dart';
import 'package:stream_chat/src/core/api/general_api.dart';
import 'package:stream_chat/src/core/api/guest_api.dart';
import 'package:stream_chat/src/core/api/message_api.dart';
import 'package:stream_chat/src/core/api/moderation_api.dart';
import 'package:stream_chat/src/core/api/user_api.dart';
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/http/token_manager.dart';
export 'device_api.dart' show PushProvider;
/// ApiClient that wraps every other specific api
class StreamChatApi {
/// Initialize a new stream chat api
StreamChatApi(
String apiKey, {
StreamHttpClient? client,
StreamHttpClientOptions? options,
TokenManager? tokenManager,
ConnectionIdManager? connectionIdManager,
AttachmentFileUploader? attachmentFileUploader,
Logger? logger,
}) : _fileUploader = attachmentFileUploader,
_client = client ??
StreamHttpClient(
apiKey,
options: options,
tokenManager: tokenManager,
connectionIdManager: connectionIdManager,
logger: logger,
);
final StreamHttpClient _client;
UserApi? _user;
/// Api dedicated to users operations
UserApi get user => _user ??= UserApi(_client);
GuestApi? _guest;
/// Api dedicated to guest operations
GuestApi get guest => _guest ??= GuestApi(_client);
MessageApi? _message;
/// Api dedicated to message operations
MessageApi get message => _message ??= MessageApi(_client);
ChannelApi? _channel;
/// Api dedicated to channel operations
ChannelApi get channel => _channel ??= ChannelApi(_client);
DeviceApi? _device;
/// Api dedicated to device operations
DeviceApi get device => _device ??= DeviceApi(_client);
ModerationApi? _moderation;
/// Api dedicated to moderation operations
ModerationApi get moderation => _moderation ??= ModerationApi(_client);
GeneralApi? _general;
/// Api dedicated to general operations
GeneralApi get general => _general ??= GeneralApi(_client);
AttachmentFileUploader? _fileUploader;
/// Class responsible for uploading images and files to a given channel
AttachmentFileUploader get fileUploader =>
_fileUploader ??= StreamAttachmentFileUploader(_client);
}
@@ -0,0 +1,49 @@
import 'dart:convert';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/user.dart';
/// Defines the api dedicated to users operations
class UserApi {
/// Initialize a new user api
UserApi(this._client);
final StreamHttpClient _client;
/// Requests users with a given query.
Future<QueryUsersResponse> queryUsers({
bool presence = false,
Filter? filter,
List<SortOption>? sort,
PaginationParams? pagination,
}) async {
final response = await _client.get(
'/users',
queryParameters: {
'payload': jsonEncode({
'presence': presence,
if (sort != null) 'sort': sort,
if (filter != null) 'filter_conditions': filter,
if (pagination != null) ...pagination.toJson(),
}),
},
);
return QueryUsersResponse.fromJson(response.data);
}
/// Batch update a list of users
Future<UpdateUsersResponse> updateUsers(
List<User> users,
) async {
final response = await _client.post(
'/users',
data: {
'users': {for (final user in users) user.id: user},
},
);
return UpdateUsersResponse.fromJson(response.data);
}
}
@@ -0,0 +1,166 @@
// ignore_for_file: lines_longer_than_80_chars
import 'package:collection/collection.dart';
/// Complete list of errors that are returned by the API
/// together with the description and API code.
enum ChatErrorCode {
// Client errors
/// Unauthenticated, token not defined
undefinedToken,
// Bad Request
/// Wrong data/parameter is sent to the API
inputError,
/// Duplicate username is sent while enforce_unique_usernames is enabled
duplicateUsername,
/// Message is too long
messageTooLong,
/// Event is not supported
eventNotSupported,
/// The feature is currently disabled
/// on the dashboard (i.e. Reactions & Replies)
channelFeatureNotSupported,
/// Multiple Levels Reply is not supported
/// the API only supports 1 level deep reply threads
multipleNestling,
/// Custom Command handler returned an error
customCommandEndpointCall,
/// App config does not have custom_action_handler_url
customCommandEndpointMissing,
// Unauthorised
/// Unauthenticated, problem with authentication
authenticationError,
/// Unauthenticated, token expired
tokenExpired,
/// Unauthenticated, token date incorrect
tokenBeforeIssuedAt,
/// Unauthenticated, token not valid yet
tokenNotValid,
/// Unauthenticated, token signature invalid
tokenSignatureInvalid,
/// Access Key invalid
accessKeyError,
// Forbidden
/// Unauthorised / forbidden to make request
notAllowed,
/// App suspended
appSuspended,
/// User tried to post a message during the cooldown period
cooldownError,
// Miscellaneous
/// Resource not found
doesNotExist,
/// Request timed out
requestTimeout,
/// Payload too big
payloadTooBig,
/// Too many requests in a certain time frame
rateLimitError,
/// Request headers are too large
maximumHeaderSizeExceeded,
/// Something goes wrong in the system
internalSystemError,
/// No access to requested channels
noAccessToChannels
}
const _errorCodeWithDescription = {
ChatErrorCode.undefinedToken:
MapEntry(1000, 'Unauthorised, token not defined'),
ChatErrorCode.inputError:
MapEntry(4, 'Wrong data/parameter is sent to the API'),
ChatErrorCode.duplicateUsername: MapEntry(6,
'Duplicate username is sent while enforce_unique_usernames is enabled'),
ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'),
ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'),
ChatErrorCode.channelFeatureNotSupported: MapEntry(19,
'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)'),
ChatErrorCode.multipleNestling: MapEntry(21,
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads'),
ChatErrorCode.customCommandEndpointCall:
MapEntry(45, 'Custom Command handler returned an error'),
ChatErrorCode.customCommandEndpointMissing:
MapEntry(44, 'App config does not have custom_action_handler_url'),
ChatErrorCode.authenticationError:
MapEntry(5, 'Unauthenticated, problem with authentication'),
ChatErrorCode.tokenExpired: MapEntry(40, 'Unauthenticated, token expired'),
ChatErrorCode.tokenBeforeIssuedAt:
MapEntry(42, 'Unauthenticated, token date incorrect'),
ChatErrorCode.tokenNotValid:
MapEntry(41, 'Unauthenticated, token not valid yet'),
ChatErrorCode.tokenSignatureInvalid:
MapEntry(43, 'Unauthenticated, token signature invalid'),
ChatErrorCode.accessKeyError: MapEntry(2, 'Access Key invalid'),
ChatErrorCode.notAllowed:
MapEntry(17, 'Unauthorised / forbidden to make request'),
ChatErrorCode.appSuspended: MapEntry(99, 'App suspended'),
ChatErrorCode.cooldownError:
MapEntry(60, 'User tried to post a message during the cooldown period'),
ChatErrorCode.doesNotExist: MapEntry(16, 'Resource not found'),
ChatErrorCode.requestTimeout: MapEntry(23, 'Request timed out'),
ChatErrorCode.payloadTooBig: MapEntry(22, 'Payload too big'),
ChatErrorCode.rateLimitError:
MapEntry(9, 'Too many requests in a certain time frame'),
ChatErrorCode.maximumHeaderSizeExceeded:
MapEntry(24, 'Request headers are too large'),
ChatErrorCode.internalSystemError:
MapEntry(-1, 'Something goes wrong in the system'),
ChatErrorCode.noAccessToChannels:
MapEntry(70, 'No access to requested channels'),
};
const _authenticationErrors = [
ChatErrorCode.undefinedToken,
ChatErrorCode.authenticationError,
ChatErrorCode.tokenExpired,
ChatErrorCode.tokenBeforeIssuedAt,
ChatErrorCode.tokenNotValid,
ChatErrorCode.tokenSignatureInvalid,
ChatErrorCode.accessKeyError,
ChatErrorCode.noAccessToChannels,
];
///
ChatErrorCode? chatErrorCodeFromCode(int code) => _errorCodeWithDescription.keys
.firstWhereOrNull((key) => _errorCodeWithDescription[key]!.key == code);
///
extension ChatErrorCodeX on ChatErrorCode {
///
String get message => _errorCodeWithDescription[this]!.value;
///
int get code => _errorCodeWithDescription[this]!.key;
///
bool get isAuthenticationError => _authenticationErrors.contains(this);
}
@@ -0,0 +1,2 @@
export 'chat_error_code.dart';
export 'stream_chat_error.dart';
@@ -0,0 +1,141 @@
import 'package:equatable/equatable.dart';
import 'package:stream_chat/src/core/error/chat_error_code.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
///
class StreamChatError with EquatableMixin implements Exception {
///
const StreamChatError(this.message);
/// Error message
final String message;
@override
List<Object?> get props => [message];
@override
String toString() => 'StreamChatError(message: $message)';
}
///
class StreamWebSocketError extends StreamChatError {
///
const StreamWebSocketError(
String message, {
this.data,
}) : super(message);
///
factory StreamWebSocketError.fromStreamError(Map<String, Object?> error) {
final data = ErrorResponse.fromJson(error);
final message = data.message ?? '';
return StreamWebSocketError(message, data: data);
}
///
factory StreamWebSocketError.fromWebSocketChannelError(
WebSocketChannelException error) {
final message = error.message ?? '';
return StreamWebSocketError(message);
}
///
int? get code => data?.code;
///
ChatErrorCode? get errorCode {
final code = this.code;
if (code == null) return null;
return chatErrorCodeFromCode(code);
}
/// Response body. please refer to [ErrorResponse].
final ErrorResponse? data;
///
bool get isRetriable => data == null;
@override
List<Object?> get props => [...super.props, code];
@override
String toString() {
var params = 'message: $message';
if (data != null) params += ', data: $data';
return 'WebSocketError($params)';
}
}
///
class StreamChatNetworkError extends StreamChatError {
///
StreamChatNetworkError(
ChatErrorCode errorCode, {
int? statusCode,
this.data,
}) : code = errorCode.code,
statusCode = statusCode ?? data?.statusCode,
super(errorCode.message);
///
StreamChatNetworkError.raw({
required this.code,
required String message,
this.statusCode,
this.data,
}) : super(message);
///
factory StreamChatNetworkError.fromDioError(DioError error) {
final response = error.response;
ErrorResponse? errorResponse;
final data = response?.data;
if (data != null) {
errorResponse = ErrorResponse.fromJson(data);
}
return StreamChatNetworkError.raw(
code: errorResponse?.code ?? -1,
message:
errorResponse?.message ?? response?.statusMessage ?? error.message,
statusCode: errorResponse?.statusCode ?? response?.statusCode,
data: errorResponse,
)..stackTrace = error.stackTrace;
}
/// Error code
final int code;
/// HTTP status code
final int? statusCode;
/// Response body. please refer to [ErrorResponse].
final ErrorResponse? data;
StackTrace? _stackTrace;
///
set stackTrace(StackTrace? stack) => _stackTrace = stack;
///
ChatErrorCode? get errorCode => chatErrorCodeFromCode(code);
///
bool get isRetriable => data == null;
@override
List<Object?> get props => [...super.props, code, statusCode];
@override
String toString({bool printStackTrace = false}) {
var params = 'code: $code, message: $message';
if (statusCode != null) params += ', statusCode: $statusCode';
if (data != null) params += ', data: $data';
var msg = 'StreamChatNetworkError($params)';
if (printStackTrace && _stackTrace != null) {
msg += '\n$_stackTrace';
}
return msg;
}
}
@@ -0,0 +1,27 @@
// ignore_for_file: use_setters_to_change_properties
/// Handles the connection id of the websocket connection
class ConnectionIdManager {
/// Initialize a new connection id manager
ConnectionIdManager({
String? connectionId,
}) : _connectionId = connectionId;
String? _connectionId;
/// Get the current connection id
String? get connectionId => _connectionId;
/// True if there is a connection id
bool get hasConnectionId => _connectionId != null;
/// Set the connection id
void setConnectionId(String connectionId) {
_connectionId = connectionId;
}
/// Clear the connection id
void reset() {
_connectionId = null;
}
}

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