Merge pull request #199 from GetStream/feature/mono-repo

Feature/mono repo
This commit is contained in:
Salvatore Giordano
2021-01-15 16:43:40 +01:00
committed by GitHub
483 changed files with 26648 additions and 583 deletions
+12 -4
View File
@@ -8,11 +8,14 @@ on:
release:
types:
- created
defaults:
run:
working-directory: packages/flutter_widgets
jobs:
build_and_deploy_ios:
runs-on: [macos-latest]
steps:
runs-on: [macos-latest]
steps:
- uses: actions/checkout@v2
- name: Install RubyGems
run: |
@@ -50,14 +53,19 @@ jobs:
- run: |
cd example
flutter pub get
- name: Decode google-services.json
env:
ANDROID_GOOGLE_SERVICES: ${{ secrets.ANDROID_GOOGLE_SERVICES }}
run: echo $ANDROID_GOOGLE_SERVICES > ./example/android/app/google-services.json
- name: Build
run: |
cd example
flutter build apk
- name: Deploy
uses: wzieba/Firebase-Distribution-Github-Action@v1.2.1
uses: wzieba/Firebase-Distribution-Github-Action@v1.3.2
with:
appId: ${{secrets.FIREBASE_ANDROID_APPID}}
token: ${{secrets.FIREBASE_TOKEN}}
groups: stream-testers
file: example/build/app/outputs/apk/release/app-release.apk
debug: true
file: packages/flutter_widgets/example/build/app/outputs/apk/release/app-release.apk
-24
View File
@@ -1,24 +0,0 @@
name: Flutter Analyze
on:
push:
branches:
- master
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
channel: 'beta'
- run: flutter pub get
- name: 'Flutter Format Check'
run: flutter format --set-exit-if-changed --dry-run .
- run: flutter analyze
+13
View File
@@ -0,0 +1,13 @@
#!/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"
+6
View File
@@ -0,0 +1,6 @@
#!/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"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
if [[ $(git ls-files --modified) ]]; then
echo ""
echo ""
echo "These files are not formatted correctly:"
for f in $(git ls-files --modified); do
echo ""
echo ""
echo "-----------------------------------------------------------------"
echo "$f"
echo "-----------------------------------------------------------------"
echo ""
git --no-pager diff --unified=0 --minimal $f
echo ""
echo "-----------------------------------------------------------------"
echo ""
echo ""
done
if [[ $GITHUB_WORKFLOW ]]; then
git checkout . > /dev/null 2>&1
fi
echo ""
echo "❌ Some files are incorrectly formatted, see above output."
echo ""
echo "To fix these locally, run: 'melos run format'."
exit 1
else
echo ""
echo "✅ All files are formatted correctly."
fi
@@ -0,0 +1,88 @@
name: stream_flutter_workflow
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
on:
pull_request:
push:
branches:
- main
paths-ignore:
- 'docs/**'
jobs:
analyze:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 0
- name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools'
run: |
./.github/workflows/scripts/install-tools.sh
flutter pub global activate tuneup
- name: 'Bootstrap Workspace'
run: melos bootstrap
- name: 'Dart Analyze'
run: |
melos exec -c 3 -- \
tuneup check
- name: 'Pub Check'
run: |
melos exec -c 1 --no-private --ignore="*example*" -- \
pub publish --dry-run
format:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 0
- name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools'
run: |
./.github/workflows/scripts/install-tools.sh
curl -sL https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar -o $HOME/google-java-format.jar
- name: 'Bootstrap Workspace'
run: melos bootstrap
- name: 'Dart'
run: |
melos exec -c 1 -- \
flutter format .
./.github/workflows/scripts/validate-formatting.sh
- name: 'Objective-C'
if: ${{ success() || failure() }}
run: |
melos exec -c 4 --ignore="*platform_interface*" --ignore="*web*" -- \
find . -maxdepth 3 -name "*.h" -o -name "*.m" -print0 \| xargs -0 clang-format -i --style=Google --verbose
./.github/workflows/scripts/validate-formatting.sh
- name: 'Java'
if: ${{ success() || failure() }}
run: |
melos exec -c 4 --ignore="*platform_interface*" --ignore="*web*" -- \
find . -maxdepth 12 -name "*.java" -print0 \| xargs -0 java -jar $HOME/google-java-format.jar --replace
./.github/workflows/scripts/validate-formatting.sh
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 0
- name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools'
run: ./.github/workflows/scripts/install-tools.sh
- name: 'Bootstrap Workspace'
run: melos bootstrap
- name: 'Flutter Test'
run: |
melos exec -c 3 --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test
-30
View File
@@ -1,30 +0,0 @@
name: Test
on:
pull_request:
jobs:
test:
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- name: Flutter action
uses: subosito/[email protected]
with:
channel: 'stable'
- name: Get dependencies
run: flutter pub get
- name: Run formatter
run: flutter format -n --set-exit-if-changed .
- name: Coverage fix
run: |
file=test/coverage_helper_test.dart
echo "// Helper file to make coverage work for all dart files\n" > $file
echo "// ignore_for_file: unused_import" >> $file
find lib -name '*.dart' | grep -e '[^g]\.dart' | grep -v '_html.dart' | cut -c4- | awk -v package=stream_chat_flutter '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file
echo "" >> $file
echo "void main(){}" >> $file
cat $file
- name: Run tests
run: flutter test --coverage
- name: Codecov
run: bash <(curl -s https://codecov.io/bash) -c -t ${{ secrets.CODECOV_TOKEN }} -f coverage/lcov.info -F flutter_tool
+41 -54
View File
@@ -1,65 +1,52 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
coverage/
coverage_helper_test.dart
# Web related
lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
flutter_export_environment.sh
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
examples/all_plugins/pubspec.yaml
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
Podfile
Podfile.lock
Pods/
.symlinks/
**/Flutter/App.framework/
**/Flutter/ephemeral/
**/Flutter/Flutter.framework/
**/Flutter/Generated.xcconfig
**/Flutter/flutter_assets/
fvm
google-services.json
example/ios/dist
.vscode/
ServiceDefinitions.json
xcuserdata/
**/DerivedData/
local.properties
keystore.properties
.gradle/
gradlew
gradlew.bat
gradle-wrapper.jar
.flutter-plugins-dependencies
*.iml
generated_plugin_registrant.dart
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
GeneratedPluginRegistrant.java
GeneratedPluginRegistrant.swift
build/
.flutter-plugins
.project
.classpath
.settings
/.fvm
.melos_tool/
/packages/flutter_widgets/example/ios/Flutter/.last_build_id
/packages/dart_client/example/ios/Flutter/.last_build_id
/packages/dart_client/example/ios/Runner.xcodeproj/project.pbxproj
+45 -155
View File
@@ -1,169 +1,59 @@
# Official Flutter SDK for [Stream Chat](https://getstream.io/chat/)
# Stream Chat Dart
<p align="center">
<a href="https://getstream.io/tutorials/ios-chat/"><img src="https://i.imgur.com/L4Mj8S2.png" alt="Flutter Chat" width="60%" /></a>
</p>
![](https://camo.githubusercontent.com/f5f074f3e1cde523ae0d425347149e20f861024d1d8e19b22053294ad85c43c8/68747470733a2f2f692e696d6775722e636f6d2f4c344d6a3853322e706e67)
> The official Flutter components for Stream Chat, a service for
> building chat applications.
This repository contains code for our [Dart](https://dart.dev/) and [Flutter](https://flutter.dev/) chat clients.
[![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter)
![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square)
[![Gitter](https://badges.gitter.im/GetStream/stream-chat-flutter.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
![CI](https://github.com/GetStream/stream-chat-flutter/workflows/CI/badge.svg?branch=master)
<img align="right" src="https://getstream.imgix.net/images/ios-chat-tutorial/iphone_chat_art@1x.png?auto=format,enhance" width="50%" />
Stream allows developers to rapidly deploy scalable feeds and chat messaging with an industry leading 99.999% uptime SLA guarantee.
**Quick Links**
## Structure
Stream Chat Dart is a monorepo built using [Melos](https://docs.page/invertase/melos). Individual packages can be found in the `packages` directory while configuration and top level commands can be found in `melos.yaml`.
- [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat
- [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/)
- [Chat UI Kit](https://getstream.io/chat/ui-kit/)
To get started, run `bootstrap` after cloning the project.
## Flutter Chat Tutorial
The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/).
It teaches you how to use this SDK and also shows how to make frequently required changes.
## Example App
This repo includes a fully functional example app with setup instructions.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/example) folder.
## Add dependency
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter)
```yaml
dependencies:
stream_chat_flutter: ^latest_version
```shell
melos bootstrap
```
You should then run `flutter packages get`
## Available Commands
### Analyze
> Requires `tuneup` to be activated globally. Please see https://pub.dev/packages/tuneup
```shell
melos run analyze
```
### Android
### Pub Lint
Runs pub publish with ``--dry-run``
```shell
melos run lint:pub
```
All set ✅
### Build iOS
Builds iOS examples without codesign
```shell
melos run build:examples:ios
```
### iOS
### Build APK
Builds an Android APK for examples
```shell
melos run build:examples:android
```
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.
### Build MACOS
Builds MacOs for all examples
```shell
melos run build:examples:macos
```
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.
### Test
Runs `flutter test` on all packages
```shell
melos run test
```
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.
### Troubleshooting
It may happen that you have some problems building the app.
If it seems related to the [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) make sure to check [this page](https://github.com/miguelpruivo/flutter_file_picker/wiki/Troubleshooting)
## Docs
### Business logic components
We provide 3 Widgets dedicated to business logic and state management:
- [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html)
- [StreamChannel](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannel-class.html)
- [ChannelsBloc](https://pub.dev/documentation/stream_chat_flutter/0.2.0-alpha+2/stream_chat_flutter/ChannelsBloc-class.html)
### UI Components
These are the available Widgets that you can use to build your application UI.
Every widget uses the `StreamChat` or `StreamChannel` widgets to manage the state and communicate with Stream services.
- [ChannelHeader](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelHeader-class.html)
- [ChannelImage](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelImage-class.html)
- [ChannelListView](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListView-class.html)
- [ChannelName](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelName-class.html)
- [ChannelPreview](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelPreview-class.html)
- [MessageInput](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageInput-class.html)
- [MessageListView](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageListView-class.html)
- [MessageWidget](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageWidget-class.html)
- [StreamChatTheme](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatTheme-class.html)
- [ThreadHeader](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ThreadHeader-class.html)
- ...
### Customizing styles
The Flutter SDK comes with a fully designed set of widgets that you can customize to fit with your application style and typography.
Changing the theme of Chat widgets works in a very similar way that `MaterialApp` and `Theme` do.
Out of the box, all chat widgets use their default styling, and there are two ways to change the styling:
1. Initialize the `StreamChatTheme` from your existing `MaterialApp` style
```dart
class MyApp extends StatelessWidget {
final Client client;
MyApp(this.client);
@override
Widget build(BuildContext context) {
final theme = ThemeData(
primarySwatch: Colors.green,
);
return MaterialApp(
theme: theme,
builder: (context, child) => StreamChat(
child: child,
client: client,
streamChatThemeData: StreamChatThemeData.fromTheme(theme),
),
home: ChannelListPage(),
);
}
}
```
2. Construct a custom theme and provide all the customizations needed
```dart
class MyApp extends StatelessWidget {
final Client client;
MyApp(this.client);
@override
Widget build(BuildContext context) {
final theme = ThemeData(
primarySwatch: Colors.green,
);
return MaterialApp(
theme: theme,
builder: (context, child) => StreamChat(
child: child,
client: client,
streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
ownMessageTheme: MessageTheme(
messageBackgroundColor: Colors.black,
messageText: TextStyle(
color: Colors.white,
),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(8),
),
),
),
),
home: ChannelListPage(),
);
}
}
```
### Offline storage
By default the library saves information about channels and messages in a SQLite DB.
Set the property `persistenceEnabled` to false if you don't want to use the offline storage.
## Contributing
We welcome code changes that improve this library or fix a problem,
please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github.
We are pleased to merge your code into the official repository.
Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first.
See our license file for more details.
### Test Web
Runs `flutter test --platform=chrome` on all packages
```shell
melos run test:web
```
+13 -63
View File
@@ -1,63 +1,13 @@
#include: package:pedantic/analysis_options.yaml
#
#analyzer:
# enable-experiment:
# - extension-methods
# exclude:
# - lib/**/*.g.dart
# - example/**
#
#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_declare_return_types
# # - always_specify_types
# # - annotate_overrides
# # - avoid_as
# - avoid_empty_else
# - avoid_init_to_null
# - avoid_return_types_on_setters
# - avoid_web_libraries_in_flutter
# - await_only_futures
# - camel_case_types
# - cancel_subscriptions
# - close_sinks
# # - comment_references # we do not presume as to what people want to reference in their dartdocs
# # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
# - control_flow_in_finally
# - empty_constructor_bodies
# - empty_statements
# - hash_and_equals
# - implementation_imports
# # - invariant_booleans
# # - iterable_contains_unrelated_type
# - library_names
# # - library_prefixes
# # - list_remove_unrelated_type
# # - literal_only_boolean_expressions
# - non_constant_identifier_names
# # - one_member_abstracts
# # - only_throw_errors
# # - overridden_fields
## - package_api_docs
# - package_names
# - package_prefixed_library_names
# - prefer_is_not_empty
# # - prefer_mixin # https://github.com/dart-lang/language/issues/32
# - public_member_api_docs
# - slash_for_doc_comments
# # - sort_constructors_first
# # - sort_unnamed_constructors_first
# # - super_goes_last # no longer needed w/ Dart 2
# - test_types_in_equals
# - throw_in_finally
# # - type_annotate_public_apis # subset of always_specify_types
# - type_init_formals
# # - unawaited_futures
# - unnecessary_brace_in_string_interps
# - unnecessary_getters_setters
# - unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
include: package:pedantic/analysis_options.1.9.0.yaml
analyzer:
exclude:
# Ignore generated files
- '**/*.g.dart'
- 'lib/src/generated/*.dart'
linter:
rules:
public_member_api_docs: true
prefer_final_in_for_each: true
prefer_final_locals: true
-235
View File
@@ -1,235 +0,0 @@
{
"project_info": {
"project_number": "674907137625",
"firebase_url": "https://stream-chat-internal.firebaseio.com",
"project_id": "stream-chat-internal",
"storage_bucket": "stream-chat-internal.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:e55b74fd5747e39ad7f348",
"android_client_info": {
"package_name": "com.example.example"
}
},
"oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:05257db0ea1f2505d7f348",
"android_client_info": {
"package_name": "io.getstream.chat.android.client.sample"
}
},
"oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:c41e48c4c799aee8d7f348",
"android_client_info": {
"package_name": "io.getstream.chat.example"
}
},
"oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:c36d6f4ab7491192d7f348",
"android_client_info": {
"package_name": "io.getstream.chat.sample"
}
},
"oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:d91b4d966e372431d7f348",
"android_client_info": {
"package_name": "io.getstream.chat.sample.debug"
}
},
"oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:674907137625:android:ff6593c705df1c43d7f348",
"android_client_info": {
"package_name": "io.getstream.chat.sdk"
}
},
"oauth_client": [
{
"client_id": "674907137625-khntpmmm3s1756v4jugnhcifgjlqrmd0.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.getstream.chat.sdk",
"certificate_hash": "38f7d663a2ed58ae8990d365c41bd82df62185dc"
}
},
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.getstream.flutter"
}
}
]
}
}
}
],
"configuration_version": "1"
}
-1
View File
@@ -1 +0,0 @@
c770358a10272b4ee1cdc2eb432445f6
+59
View File
@@ -0,0 +1,59 @@
name: stream_chat_dart
versioning:
mode: independent
packages:
- packages/**
scripts:
# - Requires `pub global activate tuneup`.
analyze: >
melos exec -c 1 --fail-fast -- \
pub global run tuneup check
format: pub global run flutter_plugin_tools format
build:examples:ios: >
melos exec -c 1 --scope="*example*" --fail-fast -- \
flutter build ios --no-codesign
build:examples:android: >
melos exec -c 1 --scope="*example*" --fail-fast -- \
flutter build apk
# 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: >
melos exec -c 1 --fail-fast --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
environment:
sdk: ">=2.1.0 <3.0.0"
flutter: ">=1.12.13+hotfix.5 <2.0.0"
+60
View File
@@ -0,0 +1,60 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
coverage/
coverage_helper_test.dart
# Web related
lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
+10
View File
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 659dc8129d4edb9166e9a0d600439d135740933f
channel: beta
project_type: package
+433
View File
@@ -0,0 +1,433 @@
## 0.2.23+3
- Remove + notation from userAgent
- Fix optimistic update for totalUnreadCount
## 0.2.23+2
- Do not throw an error when calling queryChannels without an active connection if the offline storage is enabled
## 0.2.23+1
- Throw an error when calling queryChannels without an active connection
- Wait to establish a connection if calling queryChannels while connecting
## 0.2.23
- Add thread_participants in message model
## 0.2.22
- Add thread-less message reply feature (QuotedMessage)
## 0.2.21+2
- Fix but not throwing error during querychannels and persistance disabled
- Fix reaction.updated event handling
## 0.2.21+1
- Fix error in the offline storage queryChannelCids query
## 0.2.21
- Fix channel.hide(clearHistory: true) not clearing local messages
- Add banned field to member
## 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.
## 0.2.19
- Added message filters for `Client.search()`
## 0.2.18
- Correctly dispose resources when disposing the client state
- Limit parallel queryChannels with same parameters to 1
- Added `clearUser` parameter to `client.disconnect` to remove the user instance of the client
## 0.2.17+1
- Do not retry messages when server returns error
## 0.2.17
- Add shadow ban feature
## 0.2.16
- Listen for user.updated events
## 0.2.15+2
- Fix reaction score updates
## 0.2.15+1
- Listen to reaction.updated event
## 0.2.15
- Fix search message response
## 0.2.14
- Add event.extradata
## 0.2.13+1
- Let user change channel.extradata if the channel is not initialized yet
## 0.2.13
- Add parent_id to events for typing indicators in threads
## 0.2.12+2
- Fix error with reactions with null user
## 0.2.12
- Do not save channels in memory if not being watched. This was leading to some bugs in some specific use-cases.
## 0.2.11
- Fix user.name getter
- Use detached loggers
- Throw error while connecting if it comes from backend
- Fix ws reconnection
## 0.2.10+2
- Fix bug with event filtering
## 0.2.10+1
- Add default limit to pagination
## 0.2.10
- Added `channel.state.unreadCountStream`
## 0.2.9
- Adding a message on `Channel.update` is now optional
## 0.2.8+1
- Fix retry logic
## 0.2.8
- Add missing event types
- Fix local sorting on offline storage
## 0.2.7+1
- `Client.channel` returns an existing channel if available
- Update message in the offline storage if attachment has expired (for the new CDN)
- Fix `GetMessagesByIdResponse` format
- Do not query messages if already existing in offline storage
## 0.2.6
- Experimental support for Flutter web and MacOs
## 0.2.5+2
- Cleaned up Serialization on extra_data
## 0.2.5+1
- Fix `channel.show` api call
## 0.2.5
- Add `channelType` and `channelId` properties to event object
## 0.2.4+2
- Fix query members messing channel state
## 0.2.4+1
- Do not resync if there is no channel in offlinestorage
## 0.2.4
- Add null-safety to ws disconnect
- Add pagination parameters to queryUsers request
## 0.2.3+3
- Fix reaction add/remove logic
## 0.2.3+2
- Skip system messages during unreadCount computation
## 0.2.3+1
- Removed moor_ffi from dependencies in favor of moor/ffi
## 0.2.3
- Fix reject invite payload
- Add multi-tenant properties to channel and user
## 0.2.2+1
- Fix queryChannels payload
## 0.2.2
- Fix add/remove/invite members api calls
## 0.2.1
- Add `isMutedStream` to `Channel`
- Add `isGroup` to `Channel`
- Add `isDistinct` to `Channel`
## 0.2.0+2
- Fix search messages response class
## 0.2.0+1
- Fix offline members update
- Add channel mutes
- Fix default channel sort
## 0.2.0
- Add `lastMessage` getter to Channel.state
- Add `isSystem` property to Message
- Incremental websocket reconnection timeout
- Add translate message api call
- Add queryMembers api call
- Add user list to client state
- Synchronize channel members status
- Add offline storage
- Add push notifications helper functions
## 0.2.0-alpha+23
- Add `lastMessage` getter to `Channel.state`
## 0.2.0-alpha+22
- Add `isSystem` property to Message
## 0.2.0-alpha+21
- Incremental websocket reconnection timeout
## 0.2.0-alpha+20
- More robust offline storage insertions
## 0.2.0-alpha+19
- Add translate message api call
- Add queryMembers api call
## 0.2.0-alpha+18
- Revert moor_ffi version to 0.5.0
## 0.2.0-alpha+17
- Add user list to client
- Synchronize channel members status
## 0.2.0-alpha+16
- Try QueryChannels when `resync` endpoint returns an error
## 0.2.0-alpha+15
- Fix receiving reactions
## 0.2.0-alpha+14
- Avoid sending local event for optimistic updates
## 0.2.0-alpha+13
- Fix offline on app first start up
## 0.2.0-alpha+12
- Fix retry mechanism in threads
- Fix delete channel query
## 0.2.0-alpha+9
- Add retry mechanism and retry queue
## 0.2.0-alpha+8
- Add copyWith to Attachment
## 0.2.0-alpha+7
- Add channel deleted/updated event handling
## 0.2.0-alpha+6
- Align with stable release
## 0.2.0-alpha+5
- Rename client parameters
## 0.2.0-alpha+3
- Remove dependencies on notification service
- Expose some helping method for integrate offline storage with push notifications
## 0.2.0-alpha+2
- Fix unread count
## 0.2.0-alpha
- Offline storage
- Push notifications
- Minor bug fixes
## 0.1.30
- Add silent property to message
## 0.1.29
- Fix read event handling
## 0.1.28
- Fix bug clearing members when receiving a message
## 0.1.27
- Update dependencies
## 0.1.26
- Remove wrong `members` property from `ChannelModel`
## 0.1.25
- Fix online status
## 0.1.24
- Fix unread count
## 0.1.22
- Add mute/unmute channel
## 0.1.20
- Fix channel query path without id
## 0.1.19
- Fix loading message replies
## 0.1.18
- Export dio error
## 0.1.17
- Ignore current user typing events
- Add event types
## 0.1.16
- Fix message update
## 0.1.15
- Fix mentions handling
## 0.1.14
- Handle message modification and commands
## 0.1.13
- Add message.updated event handling
## 0.1.12
- Add export multipart_file from dio
## 0.1.11
- Add channel config checks
## 0.1.10
- Rename Channel.channelClients to channels
## 0.1.9
- Fix channel update on message delete
## 0.1.8
- Add delete message handling
## 0.1.7
- Add reaction handling
## 0.1.6
- Add initialized completer
- Update example
## 0.1.5
- Add `ClientState` and `ChannelClientState` classes to handle channel state updates using events
- Update example supporting threads
## 0.1.4
- Update some api with wrong or incomplete signatures
- Add documentation for public apis
## 0.1.2
- add websocket reconnection logic
- add token expiration mechanism
## 0.1.1
- add typing events handling
## 0.1.0
- a better example can be found in the example/ directory
- fix some api calls and add missing one
## 0.0.2
- first beta version
+219
View File
@@ -0,0 +1,219 @@
SOURCE CODE LICENSE AGREEMENT
IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR
ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT.
THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE
BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE
LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN
INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN
EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE
OF THE SOFTWARE BY CUSTOMER FOR CUSTOMERS BUSINESS PURPOSES AS DESCRIBED IN
AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO
THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND
CUSTOMER TO THIS AGREEMENT.
STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING
CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A
COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS
AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE
USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU
REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF
STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE
READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE
BOUND BY ALL THE TERMS OF THIS AGREEMENT.
IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT,
STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO
NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND
CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE
SOFTWARE.
1. SOFTWARE. The Stream.io software accompanying this Agreement, may include
Source Code, Executable Object Code, associated media, printed materials and
documentation (collectively, the “Software”). The Software also includes any
updates or upgrades to or new versions of the original Software, if and when
made available to you by Stream.io. “Source Code” means computer programming
code in human readable form that is not suitable for machine execution without
the intervening steps of interpretation or compilation. “Executable Object
Code" means the computer programming code in any other form than Source Code
that is not readily perceivable by humans and suitable for machine execution
without the intervening steps of interpretation or compilation. “Site” means a
Customer location controlled by Customer. “Authorized User” means any employee
or contractor of Customer working at the Site, who has signed a written
confidentiality agreement with Customer or is otherwise bound in writing by
confidentiality and use obligations at least as restrictive as those imposed
under this Agreement.
2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in
consideration for the representations, warranties, and covenants made by
Customer in this Agreement, Stream.io grants to Customer, during the term of
this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable
license to:
a. install and use Software Source Code on password protected computers at a Site,
restricted to Authorized Users;
b. create derivative works, improvements (whether or not patentable), extensions
and other modifications to the Software Source Code (“Modifications”) to build
unique scalable newsfeeds, activity streams, and in-app messaging via Streams
application program interface (“API”);
c. compile the Software Source Code to create Executable Object Code versions of
the Software Source Code and Modifications to build such newsfeeds, activity
streams, and in-app messaging via the API;
d. install, execute and use such Executable Object Code versions solely for
Customers internal business use (including development of websites through
which data generated by Stream services will be streamed (“Apps”));
e. use and distribute such Executable Object Code as part of Customers Apps; and
f. make electronic copies of the Software and Modifications as required for backup
or archival purposes.
3. RESTRICTIONS. Customer is responsible for all activities that occur in
connection with the Software. Customer will not, and will not attempt to: (a)
sublicense or transfer the Software or any Source Code related to the Software
or any of Customers rights under this Agreement, except as otherwise provided
in this Agreement, (b) use the Software Source Code for the benefit of a third
party or to operate a service; (c) allow any third party to access or use the
Software Source Code; (d) sublicense or distribute the Software Source Code or
any Modifications in Source Code or other derivative works based on any part of
the Software Source Code; (e) use the Software in any manner that competes with
Stream.io or its business; or (e) otherwise use the Software in any manner that
exceeds the scope of use permitted in this Agreement. Customer shall use the
Software in compliance with any accompanying documentation any laws applicable
to Customer.
4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or
software components that are open source in conjunction with the Software
Source Code or any Modifications in Source Code or in any way that could
subject the Software to any open source licenses.
5. CONTRACTORS. Under the rights granted to Customer under this Agreement,
Customer may permit its employees, contractors, and agencies of Customer to
become Authorized Users to exercise the rights to the Software granted to
Customer in accordance with this Agreement solely on behalf of Customer to
provide services to Customer; provided that Customer shall be liable for the
acts and omissions of all Authorized Users to the extent any of such acts or
omissions, if performed by Customer, would constitute a breach of, or otherwise
give rise to liability to Customer under, this Agreement. Customer shall not
and shall not permit any Authorized User to use the Software except as
expressly permitted in this Agreement.
6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way
to engage in the development of products or services which could be reasonably
construed to provide a complete or partial functional or commercial alternative
to Stream.ios products or services (a “Competitive Product”). Customer shall
ensure that there is no direct or indirect use of, or sharing of, Software
source code, or other information based upon or derived from the Software to
develop such products or services. Without derogating from the generality of
the foregoing, development of Competitive Products shall include having direct
or indirect access to, supervising, consulting or assisting in the development
of, or producing any specifications, documentation, object code or source code
for, all or part of a Competitive Product.
7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement,
Modifications may only be created and used by Customer as permitted by this
Agreement and Modification Source Code may not be distributed to third parties.
Customer will not assert against Stream.io, its affiliates, or their customers,
direct or indirect, agents and contractors, in any way, any patent rights that
Customer may obtain relating to any Modifications for Stream.io, its
affiliates, or their customers, direct or indirect, agents and contractors
manufacture, use, import, offer for sale or sale of any Stream.io products or
services.
8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant
to Stream.io standard download procedures. The Software is deemed accepted upon
delivery.
9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to
provide any support or consultation concerning the Software.
10. TERM AND TERMINATION. The term of this Agreement begins when the Software is
downloaded or accessed and shall continue until terminated. Either party may
terminate this Agreement upon written notice. This Agreement shall
automatically terminate if Customer is or becomes a competitor of Stream.io or
makes or sells any Competitive Products. Upon termination of this Agreement for
any reason, (a) all rights granted to Customer in this Agreement immediately
cease to exist, (b) Customer must promptly discontinue all use of the Software
and return to Stream.io or destroy all copies of the Software in Customers
possession or control. Any continued use of the Software by Customer or attempt
by Customer to exercise any rights under this Agreement after this Agreement
has terminated shall be considered copyright infringement and subject Customer
to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9
shall survive expiration or termination of this Agreement for any reason.
11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual
property rights and proprietary rights relating thereto or embodied therein,
are the exclusive property of Stream.io and its suppliers. Stream.io and its
suppliers reserve all rights in and to the Software not expressly granted to
Customer in this Agreement, and no other licenses or rights are granted by
implication, estoppel or otherwise.
12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMERS
OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND
WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY
KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT
LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS,
QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS
ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED
THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS
SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO
MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND
DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW.
CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE
EXPRESS WARRANTIES IN THIS AGREEMENT.
13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IOS
TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR
THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE,
SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT,
CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND
WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING
TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON
ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO
THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY.
14. General. Customer may not assign or transfer this Agreement, by operation of
law or otherwise, or any of its rights under this Agreement (including the
license rights granted to Customer) to any third party without Stream.ios
prior written consent, which consent will not be unreasonably withheld or
delayed. Stream.io may assign this Agreement, without consent, including, but
limited to, affiliate or any successor to all or substantially all its business
or assets to which this Agreement relates, whether by merger, sale of assets,
sale of stock, reorganization or otherwise. Any attempted assignment or
transfer in violation of the foregoing will be null and void. Stream.io shall
not be liable hereunder by reason of any failure or delay in the performance of
its obligations hereunder for any cause which is beyond the reasonable control.
All notices, consents, and approvals under this Agreement must be delivered in
writing by courier, by electronic mail, or by certified or registered mail,
(postage prepaid and return receipt requested) to the other party at the
address set forth in the customer agreement between Stream.io and Customer and
will be effective upon receipt or when delivery is refused. This Agreement will
be governed by and interpreted in accordance with the laws of the State of
Colorado, without reference to its choice of laws rules. The United Nations
Convention on Contracts for the International Sale of Goods does not apply to
this Agreement. Any action or proceeding arising from or relating to this
Agreement shall be brought in a federal or state court in Denver, Colorado, and
each party irrevocably submits to the jurisdiction and venue of any such court
in any such action or proceeding. All waivers must be in writing. Any waiver or
failure to enforce any provision of this Agreement on one occasion will not be
deemed a waiver of any other provision or of such provision on any other
occasion. If any provision of this Agreement is unenforceable, such provision
will be changed and interpreted to accomplish the objectives of such provision
to the greatest extent possible under applicable law and the remaining
provisions will continue in full force and effect. Customer shall not violate
any applicable law, rule or regulation, including those regarding the export of
technical data. The headings of Sections of this Agreement are for convenience
and are not to be used in interpreting this Agreement. As used in this
Agreement, the word “including” means “including but not limited to.” This
Agreement (including all exhibits and attachments) constitutes the entire
agreement between the parties regarding the subject hereof and supersedes all
prior or contemporaneous agreements, understandings and communication, whether
written or oral. This Agreement may be amended only by a written document
signed by both parties. The terms of any purchase order or similar document
submitted by Customer to Stream.io will have no effect.
+107
View File
@@ -0,0 +1,107 @@
# Stream Chat Dart
[![Pub](https://img.shields.io/pub/v/stream_chat.svg)](https://pub.dartlang.org/packages/stream_chat)
![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square)
![CI](https://github.com/GetStream/stream-chat-dart/workflows/CI/badge.svg?branch=master)
[![codecov](https://codecov.io/gh/GetStream/stream-chat-dart/branch/master/graph/badge.svg)](https://codecov.io/gh/GetStream/stream-chat-dart)
stream-chat-dart is the official Dart client for Stream Chat, a service for building chat applications. This library can be used on any Dart project and on both mobile and web apps with Flutter.
You can sign up for a Stream account at https://getstream.io/chat/
## Getting started
### Add dependency
```yaml
dependencies:
stream_chat: ^0.2.0
```
You should then run `flutter packages get`
## Example Project
There is a detailed Flutter example project in the `example` folder. You can directly run and play on it.
## Setup API Client
First you need to instantiate a chat client. The Chat client will manage API call, event handling and manage the websocket connection to Stream Chat servers. You should only create the client once and re-use it across your application.
```dart
final client = Client("stream-chat-api-key");
```
### Logging
By default the Chat Client will write all messages with level Warn or Error to stdout.
#### Change Logging Level
During development you might want to enable more logging information, you can change the default log level when constructing the client.
```dart
final client = Client("stream-chat-api-key", logLevel: Level.INFO);
```
#### Custom Logger
You can handle the log messages directly instead of have them written to stdout, this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility.
```dart
myLogHandlerFunction = (LogRecord record) {
// do something with the record (ie. send it to Sentry or Fabric)
}
final client = Client("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
```
### Offline storage
By default the library saves information about channels and messages in a SQLite DB.
Set the property `persistenceEnabled` to false if you don't want to use the offline storage.
## Flutter Web
Due to Moor web (for offline storage) you need to include the sql.js library:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script defer src="sql-wasm.js"></script>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body></body>
</html>
```
You can grab the latest version of sql-wasm.js and sql-wasm.wasm [here](https://github.com/sql-js/sql.js/releases) and copy them into your `/web` folder.
## Contributing
### Code conventions
- Make sure that you run `dartfmt` before commiting your code
- Make sure all public methods and functions are well documented
### Running tests
- run `flutter test`
### Releasing a new version
- update the package version on `pubspec.yaml` and `version.dart`
- add a changelog entry on `CHANGELOG.md`
- run `flutter pub publish` to publish the package
### Watch models and generate JSON code
JSON serialization relies on code generation; make sure to keep that running while you make changes to the library
```bash
flutter pub run build_runner watch
```
@@ -0,0 +1,62 @@
include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
- lib/**/*.g.dart
- example/*
- test/*
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_declare_return_types
# - always_specify_types
# - annotate_overrides
# - avoid_as
- avoid_empty_else
- avoid_init_to_null
- avoid_return_types_on_setters
- avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
# - comment_references # we do not presume as to what people want to reference in their dartdocs
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
# - invariant_booleans
# - iterable_contains_unrelated_type
- library_names
# - library_prefixes
# - list_remove_unrelated_type
# - literal_only_boolean_expressions
- non_constant_identifier_names
# - one_member_abstracts
# - only_throw_errors
# - overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
- public_member_api_docs
- slash_for_doc_comments
# - sort_constructors_first
# - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2
- test_types_in_equals
- throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types
- type_init_formals
# - unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
+11
View File
@@ -0,0 +1,11 @@
targets:
$default:
builders:
moor_generator:
options:
generate_connect_constructor: true
json_serializable:
options:
explicit_to_json: true
field_rename: snake
any_map: true
+1
View File
@@ -0,0 +1 @@
# Stream Chat Example App
@@ -0,0 +1,67 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.stream_chat"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.stream_chat">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,31 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.stream_chat">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="io.flutter.app.FlutterApplication"
android:label="stream_chat"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,12 @@
package com.example.stream_chat
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.stream_chat">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>stream_chat</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:timeago/timeago.dart' as timeago;
import './channel_name_text.dart';
import 'channel_image.dart';
import 'stream_channel.dart';
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final bool showBackButton;
final VoidCallback onBackPressed;
ChannelHeader({
Key key,
this.showBackButton = true,
this.onBackPressed,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
final streamChat = StreamChannel.of(context);
return AppBar(
leading: showBackButton ? _buildBackButton(context) : Container(),
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: ChannelImage(channel: streamChat.channel),
),
],
centerTitle: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ChannelNameText(
channel: streamChat.channel,
),
_buildLastActive(context, streamChat.channel),
],
),
);
}
StatelessWidget _buildLastActive(BuildContext context, Channel channel) {
return (channel.lastMessageAt != null)
? Text(
'Active ${timeago.format(channel.lastMessageAt)}',
style: Theme.of(context).textTheme.caption,
)
: Container();
}
Padding _buildBackButton(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(14.0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
if (onBackPressed != null) {
onBackPressed();
} else {
Navigator.of(context).pop();
}
},
fillColor: Colors.black.withOpacity(.1),
padding: EdgeInsets.all(4),
child: Icon(
Icons.arrow_back_ios,
size: 15,
color: Colors.black,
),
),
);
}
@override
final Size preferredSize;
}
@@ -0,0 +1,25 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
class ChannelImage extends StatelessWidget {
const ChannelImage({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 20,
backgroundImage: channel.extraData.containsKey('image')
? CachedNetworkImageProvider(channel.extraData['image'] as String)
: null,
child: channel.extraData.containsKey('image')
? null
: Text(channel.config.name[0]),
);
}
}
@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
import './connection_indicator.dart';
import './message_page.dart';
import './stream_channel.dart';
import 'channel_list_view.dart';
import 'channel_page_app_bar.dart';
import 'stream_chat.dart';
class ChannelListPage extends StatefulWidget {
ChannelListPage({
this.filter,
this.options,
this.sort,
this.pagination,
});
final Map<String, dynamic> filter;
final Map<String, dynamic> options;
final List<SortOption> sort;
final PaginationParams pagination;
@override
ChannelListPageState createState() => ChannelListPageState();
}
class ChannelListPageState extends State<ChannelListPage> {
String _selectedChannelId;
bool showSplit;
IndicatorController _indicatorController = IndicatorController();
@override
Widget build(BuildContext context) {
showSplit = MediaQuery.of(context).size.width > 1000;
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: 1,
child: Scaffold(
bottomNavigationBar: ConnectionIndicator(
indicatorController: _indicatorController,
),
appBar: ChannelPageAppBar(),
body: ChannelListView(
channelWidget: MessagePage(),
options: widget.options,
filter: widget.filter,
pagination: widget.pagination,
sort: widget.sort,
onChannelTap: showSplit
? (channelClient, _) {
_navigateToChannel(context, channelClient);
}
: null,
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.white,
child: Icon(
Icons.send,
),
),
),
),
showSplit ? _buildMessageView(context) : Container(),
],
);
}
Flexible _buildMessageView(BuildContext context) {
return Flexible(
flex: 2,
child: _selectedChannelId == null
? Scaffold(
body: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline,
),
),
)
: StreamChannel(
channelClient: StreamChat.of(context)
.client
.state
.channels
.firstWhere((c) => c.id == _selectedChannelId),
child: MessagePage(),
),
);
}
void _navigateToChannel(
BuildContext context,
Channel channel,
) {
setState(() {
_selectedChannelId = channel.id;
});
}
@override
void initState() {
super.initState();
final streamChat = StreamChat.of(context);
streamChat.client.wsConnectionStatus.addListener(() {
if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
_indicatorController.showIndicator(
duration: Duration(minutes: 1),
color: Colors.red,
text: 'Disconnected',
);
} else if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.connecting) {
_indicatorController.showIndicator(
duration: Duration(minutes: 1),
color: Colors.yellow,
text: 'Reconnecting',
);
} else if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.connected) {
_indicatorController.showIndicator(
duration: Duration(seconds: 5),
color: Colors.green,
text: 'Connected',
);
streamChat.clearChannels();
streamChat.queryChannels();
}
});
}
}
@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'channel_preview.dart';
import 'stream_channel.dart';
import 'stream_chat.dart';
typedef ChannelTapCallback = void Function(Channel, Widget);
class ChannelListView extends StatefulWidget {
ChannelListView({
Key key,
this.filter,
this.options,
this.sort,
this.pagination,
this.onChannelTap,
this.channelWidget,
this.channelPreview,
}) : assert(channelWidget != null || onChannelTap != null),
super(key: key);
final Map<String, dynamic> filter;
final Map<String, dynamic> options;
final List<SortOption> sort;
final PaginationParams pagination;
final ScrollController _scrollController = ScrollController();
final ChannelTapCallback onChannelTap;
final Widget channelWidget;
final Widget channelPreview;
@override
_ChannelListViewState createState() => _ChannelListViewState();
}
class _ChannelListViewState extends State<ChannelListView> {
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
return RefreshIndicator(
onRefresh: () async {
streamChat.clearChannels();
return streamChat.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
},
child: StreamBuilder<List<Channel>>(
stream: streamChat.client.state.channelsStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
print((snapshot.error as Error).stackTrace);
return Center(
child: Text(snapshot.error.toString()),
);
}
final channelsStates = snapshot.data;
return ListView.custom(
physics: AlwaysScrollableScrollPhysics(),
controller: widget._scrollController,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
return _itemBuilder(context, i, channelsStates);
},
childCount: (channelsStates.length * 2) + 1,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index = channelsStates
.indexWhere((cs) => 'CHANNEL-${cs.id}' == valueKey.value);
return index != -1 ? (index * 2) : null;
},
),
);
}),
);
}
Widget _itemBuilder(context, int i, List<Channel> channelsStates) {
if (i % 2 != 0) {
return _separatorBuilder(context, i);
}
i = i ~/ 2;
final streamChat = StreamChat.of(context);
if (i < channelsStates.length) {
final channelState = channelsStates[i];
final channelClient = streamChat.client.state.channels
.firstWhere((c) => c.cid == channelState.cid);
ChannelTapCallback onTap;
if (widget.onChannelTap != null) {
onTap = widget.onChannelTap;
} else {
onTap = (client, _) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: widget.channelWidget,
channelClient: client,
);
},
),
);
};
}
Widget child;
if (widget.channelPreview != null) {
child = Stack(
children: [
widget.channelPreview,
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
onTap(channelClient, widget.channelWidget);
},
),
),
),
],
);
} else {
child = ChannelPreview(
onTap: (channelClient) {
onTap(channelClient, widget.channelWidget);
},
);
}
return StreamChannel(
key: ValueKey<String>('CHANNEL-${channelClient?.id}'),
child: child,
channelClient: channelClient,
);
} else {
return _buildQueryProgressIndicator(context, streamChat);
}
}
Widget _buildQueryProgressIndicator(context, StreamChat streamChat) {
return StreamBuilder<bool>(
stream: streamChat.queryChannelsLoading,
initialData: false,
builder: (context, snapshot) {
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _separatorBuilder(context, i) {
return Container(
height: 1,
color: Colors.black.withOpacity(0.1),
margin: EdgeInsets.symmetric(horizontal: 16),
);
}
void _listenChannelPagination(StreamChat streamChat) {
if (widget._scrollController.position.maxScrollExtent ==
widget._scrollController.position.pixels) {
streamChat.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith(
offset: streamChat.channels.length,
),
options: widget.options,
);
}
}
@override
void initState() {
super.initState();
final streamChat = StreamChat.of(context);
streamChat.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
widget._scrollController.addListener(() {
_listenChannelPagination(streamChat);
});
}
}
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
class ChannelNameText extends StatelessWidget {
const ChannelNameText({
Key key,
this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return Text(
channel.extraData['name'] as String ?? channel.config.name,
style: Theme.of(context).textTheme.body2,
);
}
}
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class ChannelPageAppBar extends StatelessWidget implements PreferredSizeWidget {
ChannelPageAppBar({
Key key,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
return SafeArea(
child: Material(
elevation: 4,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 30),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(5),
borderRadius: BorderRadius.circular(32.0),
border: Border.all(color: Colors.black.withOpacity(.2))),
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: TextField(
style: Theme.of(context).textTheme.body1,
autofocus: false,
decoration: InputDecoration(
hintText: 'Search',
prefixText: ' ',
border: InputBorder.none,
),
),
),
),
),
),
);
}
@override
final Size preferredSize;
}
@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
import 'channel_image.dart';
import 'channel_name_text.dart';
import 'stream_channel.dart';
import 'stream_chat.dart';
class ChannelPreview extends StatelessWidget {
final void Function(Channel) onTap;
const ChannelPreview({
Key key,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
return _buildChannelPreview(
context,
streamChannel,
);
}
StreamChannel _buildChannelPreview(
BuildContext context,
StreamChannelState streamChannel,
) {
final channelClient = StreamChat.of(context)
.client
.state
.channels
.firstWhere((c) => c.cid == streamChannel.channel.cid);
return StreamChannel(
channelClient: channelClient,
child: ListTile(
onTap: () {
onTap(channelClient);
},
leading: ChannelImage(
channel: streamChannel.channel,
),
title: ChannelNameText(
channel: streamChannel.channel,
),
subtitle: _buildSubtitle(
streamChannel,
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
_buildDate(context, streamChannel.channel.lastMessageAt),
],
),
),
);
}
Text _buildDate(BuildContext context, DateTime lastMessageAt) {
String stringDate;
final now = DateTime.now();
if (now.year != lastMessageAt.year ||
now.month != lastMessageAt.month ||
now.day != lastMessageAt.day) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
}
return Text(
stringDate,
style: Theme.of(context).textTheme.caption,
);
}
Widget _buildSubtitle(
StreamChannelState streamChannel,
) {
return StreamBuilder<List<User>>(
stream: streamChannel.channelClient.state.typingEventsStream,
initialData: [],
builder: (context, snapshot) {
final typings = snapshot.data;
final opacity =
streamChannel.channelClient.state.unreadCount > .0 ? 1.0 : 0.5;
return typings.isNotEmpty
? _buildTypings(typings, context, opacity)
: _buildLastMessage(context, streamChannel, opacity);
});
}
Widget _buildLastMessage(
BuildContext context, StreamChannelState streamChannel, double opacity) {
final lastMessage = streamChannel.channel.state.messages.isNotEmpty
? streamChannel.channel.state.messages.last
: null;
if (lastMessage == null) {
return SizedBox.fromSize(
size: Size.zero,
);
}
final prefix = lastMessage.attachments
.map((e) {
if (e.type == 'image') {
return '📷';
} else if (e.type == 'video') {
return '🎬';
}
return null;
})
.where((e) => e != null)
.join(' ');
return Text(
'$prefix ${lastMessage.text ?? ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.black.withOpacity(opacity),
),
);
}
Text _buildTypings(List<User> typings, BuildContext context, double opacity) {
return Text(
'${typings.map((u) => u.extraData.containsKey('name') ? u.extraData['name'] : u.id).join(',')} ${typings.length == 1 ? 'is' : 'are'} typing...',
maxLines: 1,
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.black.withOpacity(opacity),
),
);
}
}
@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
class ConnectionIndicator extends StatefulWidget {
final IndicatorController indicatorController;
const ConnectionIndicator({
Key key,
this.indicatorController,
}) : super(key: key);
@override
_ConnectionIndicatorState createState() => _ConnectionIndicatorState();
}
class _ConnectionIndicatorState extends State<ConnectionIndicator> {
double _height = 0;
String _text;
Color _color;
VoidCallback _listener;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: _height,
child: Container(
width: double.infinity,
child: Material(
color: _color ?? Theme.of(context).snackBarTheme.backgroundColor,
child: Center(
child: Text(_text ?? ''),
),
),
),
);
}
@override
void initState() {
super.initState();
_listener = () {
final values = widget.indicatorController.indicatorValues.value;
if (mounted) {
setState(() {
_height = 30;
_text = values.text;
_color = values.color;
});
}
if (values.duration != null) {
Future.delayed(values.duration, () {
if (mounted) {
setState(() {
_height = 0;
_text = '';
});
}
});
}
};
widget.indicatorController.indicatorValues.addListener(_listener);
}
@override
void dispose() {
widget.indicatorController.indicatorValues.removeListener(_listener);
super.dispose();
}
}
class IndicatorValues {
final String text;
final Color color;
final Duration duration;
IndicatorValues({
this.duration,
this.text,
this.color,
});
}
class IndicatorController {
ValueNotifier<IndicatorValues> indicatorValues = ValueNotifier(null);
void showIndicator({
Duration duration,
Color color,
String text,
}) {
indicatorValues.value = IndicatorValues(
text: text,
color: color,
duration: duration,
);
}
}
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'channel_list_page.dart';
import 'stream_chat.dart';
void main() async {
final client = Client(
"qk4nn7rpcn75",
logLevel: Level.INFO,
);
await client.setUser(
User(id: "wild-breeze-7"),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo',
);
runApp(StreamChat(
child: MyApp(),
client: client,
));
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Stream Chat Example',
home: ChatLoader(),
theme: ThemeData(
scaffoldBackgroundColor: Color(0xfff1f1f3),
primaryColor: Color(0xfff1f1f3),
accentColor: Color(0xff006bff),
iconTheme: IconThemeData(
color: Color(0xff006bff),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
foregroundColor: Color(0xff006bff),
),
backgroundColor: Color(0xfff1f1f3),
canvasColor: Color(0xfff1f1f3),
),
);
}
@override
void dispose() {
StreamChat.of(context).dispose();
super.dispose();
}
}
class ChatLoader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
return StreamBuilder<User>(
stream: streamChat.userStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Text('${snapshot.error}'),
),
);
} else if (!snapshot.hasData) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} else {
return ChannelListPage(
filter: {
'members': {
'\$in': [StreamChat.of(context).user.id],
}
},
sort: [SortOption("last_message_at")],
pagination: PaginationParams(
limit: 20,
),
);
}
},
);
}
}
@@ -0,0 +1,137 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_channel.dart';
class MessageInput extends StatefulWidget {
MessageInput({
Key key,
this.onMessageSent,
this.parent,
}) : super(key: key);
final void Function(Message) onMessageSent;
final Message parent;
@override
_MessageInputState createState() => _MessageInputState();
}
class _MessageInputState extends State<MessageInput> {
final _textController = TextEditingController();
bool _messageIsPresent = false;
bool _typingStarted = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
gradient: _typingStarted
? LinearGradient(colors: [Color(0xFF00AEFF), Color(0xFF0076FF)])
: null,
),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
borderRadius: BorderRadius.circular(10.0),
),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(5),
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.black.withOpacity(.2)),
),
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: TextField(
minLines: null,
maxLines: null,
onSubmitted: (_) {
_sendMessage(context);
},
controller: _textController,
onChanged: (s) {
StreamChannel.of(context).channelClient.keyStroke();
setState(() {
_messageIsPresent = s.trim().isNotEmpty;
});
},
onTap: () {
setState(() {
_typingStarted = true;
});
},
style: Theme.of(context).textTheme.body1,
autofocus: false,
decoration: InputDecoration(
hintText: 'Write a message',
prefixText: ' ',
border: InputBorder.none,
),
),
),
AnimatedCrossFade(
crossFadeState: _messageIsPresent
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: Container(),
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
),
],
),
),
),
),
);
}
IconButton _buildSendButton(BuildContext context) {
return IconButton(
onPressed: () {
_sendMessage(context);
},
icon: Icon(
Icons.send,
),
);
}
void _sendMessage(BuildContext context) {
final text = _textController.text.trim();
if (text.isEmpty) {
return;
}
_textController.clear();
setState(() {
_messageIsPresent = false;
_typingStarted = false;
});
FocusScope.of(context).unfocus();
StreamChannel.of(context)
.channelClient
.sendMessage(
Message(
parentId: widget.parent?.id,
text: text,
),
)
.then((_) {
if (widget.onMessageSent != null) {
widget.onMessageSent(Message(text: text));
}
});
}
}
@@ -0,0 +1,280 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_widgets/flutter_widgets.dart';
import 'package:stream_chat/stream_chat.dart';
import 'message_widget.dart';
import 'stream_channel.dart';
typedef MessageBuilder = Widget Function(BuildContext, Message, int index);
typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
typedef OnThreadSelectCallback = void Function(Message parent);
class MessageListView extends StatefulWidget {
MessageListView({
Key key,
MessageBuilder messageBuilder,
this.parentMessageBuilder,
this.parentMessage,
this.onThreadSelect,
}) : _messageBuilder = messageBuilder,
super(key: key);
final MessageBuilder _messageBuilder;
final ParentMessageBuilder parentMessageBuilder;
final OnThreadSelectCallback onThreadSelect;
final Message parentMessage;
@override
_MessageListViewState createState() => _MessageListViewState();
}
class _MessageListViewState extends State<MessageListView> {
static const _newMessageLoadingOffset = 100;
final ScrollController _scrollController = ScrollController();
bool _isBottom = true;
bool _topWasVisible = false;
List<Message> _messages = [];
List<Message> _newMessageList = [];
@override
Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
/// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed
return NotificationListener<ScrollNotification>(
onNotification: (_) {
if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) {
setState(() {
_messages.insertAll(0, _newMessageList);
_newMessageList.clear();
});
}
return true;
},
child: ListView.custom(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
reverse: true,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
if (i == _messages.length + 1) {
if (widget.parentMessage != null) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context, widget.parentMessage);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessageWidget(
key: ValueKey<String>(
'PARENT-MESSAGE-${widget.parentMessage.id}'),
previousMessage: null,
message: widget.parentMessage.copyWith(replyCount: 0),
nextMessage: null,
onThreadSelect: widget.onThreadSelect,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Container(
padding: const EdgeInsets.all(8),
child: Text(
'Start of a new thread',
textAlign: TextAlign.center,
),
color: Theme.of(context).primaryColorLight,
),
),
],
);
}
} else {
return SizedBox();
}
}
if (i == _messages.length) {
return _buildLoadingIndicator(streamChannel);
}
final message = _messages[i];
if (widget._messageBuilder != null) {
return widget._messageBuilder(context, message, i);
}
final previousMessage =
i < _messages.length - 1 ? _messages[i + 1] : null;
final nextMessage = i > 0 ? _messages[i - 1] : null;
if (i == 0) {
return _buildBottomMessage(
streamChannel,
previousMessage,
message,
context,
);
}
if (i == _messages.length - 1) {
return _buildTopMessage(
message,
nextMessage,
streamChannel,
context,
);
}
return MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: previousMessage,
message: message,
nextMessage: nextMessage,
onThreadSelect: widget.onThreadSelect,
);
},
childCount: _messages.length + 2,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index = _messages
.indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
return index != -1 ? index : null;
},
),
),
);
}
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
return Container(
height: 50,
child: StreamBuilder<bool>(
stream: streamChannel.queryMessage,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
print((snapshot.error as Error).stackTrace.toString());
return Center(
child: Text(snapshot.error.toString()),
);
}
if (!snapshot.data) {
return Container();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
);
}),
);
}
Widget _buildTopMessage(
Message message,
Message nextMessage,
StreamChannelState streamChannelState,
BuildContext context,
) {
return VisibilityDetector(
key: ValueKey<String>('TOP-MESSAGE'),
child: MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: null,
message: message,
nextMessage: nextMessage,
onThreadSelect: widget.onThreadSelect,
),
onVisibilityChanged: (visibility) {
final topIsVisible = visibility.visibleBounds != Rect.zero;
if (topIsVisible && !_topWasVisible) {
streamChannelState.queryMessages();
}
_topWasVisible = topIsVisible;
},
);
}
Widget _buildBottomMessage(
StreamChannelState channelBloc,
Message previousMessage,
Message message,
BuildContext context,
) {
return VisibilityDetector(
key: ValueKey<String>('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) {
_isBottom = visibility.visibleBounds != Rect.zero;
if (_isBottom) {
if (channelBloc.channelClient.state.unreadCount > 0) {
channelBloc.channelClient.markRead();
}
}
},
child: MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: previousMessage,
message: message,
nextMessage: null,
onThreadSelect: widget.onThreadSelect,
),
);
}
StreamSubscription _streamListener;
@override
void initState() {
super.initState();
final streamChannel = StreamChannel.of(context);
if (streamChannel.channelClient.state.unreadCount > 0) {
streamChannel.channelClient.markRead();
}
Stream<List<Message>> stream;
if (widget.parentMessage == null) {
stream = streamChannel.channelStateStream.map((c) => c.messages);
} else {
streamChannel.getReplies(widget.parentMessage.id);
stream = streamChannel.channelClient.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id]);
}
_streamListener = stream.listen((newMessages) {
newMessages = newMessages.reversed.toList();
if (_messages.isEmpty || newMessages.first.id != _messages.first.id) {
if (!_scrollController.hasClients ||
_scrollController.offset < _newMessageLoadingOffset) {
setState(() {
_messages = newMessages;
});
} else if (newMessages.first.user.id ==
streamChannel.channelClient.client.state.user.id) {
_scrollController.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_messages = newMessages;
});
});
} else {
_newMessageList = newMessages;
}
} else {
setState(() {
_messages = newMessages;
});
}
});
}
@override
void dispose() {
_streamListener.cancel();
super.dispose();
}
}
@@ -0,0 +1,110 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import './channel_header.dart';
import 'connection_indicator.dart';
import 'message_input.dart';
import 'message_list_view.dart';
import 'stream_channel.dart';
import 'stream_chat.dart';
class MessagePage extends StatefulWidget {
final PreferredSizeWidget _channelHeader;
const MessagePage({
Key key,
PreferredSizeWidget channelHeader,
this.parentMessage,
}) : _channelHeader = channelHeader,
super(key: key);
final Message parentMessage;
@override
_MessagePageState createState() => _MessagePageState();
}
class _MessagePageState extends State<MessagePage> {
IndicatorController _indicatorController = IndicatorController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: widget._channelHeader ?? ChannelHeader(),
body: Column(
children: <Widget>[
ConnectionIndicator(
indicatorController: _indicatorController,
),
Expanded(
child: MessageListView(
parentMessage: widget.parentMessage,
onThreadSelect: (message) {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (_, __, ___) => StreamChannel(
channelClient: StreamChannel.of(context).channelClient,
child: MessagePage(
parentMessage: message,
channelHeader: widget._channelHeader,
),
),
transitionsBuilder: (
_,
animation,
secondaryAnimation,
child,
) =>
SharedAxisTransition(
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
),
),
);
},
),
),
MessageInput(),
],
),
);
}
@override
void initState() {
super.initState();
final streamChat = StreamChat.of(context);
streamChat.client.wsConnectionStatus.addListener(() {
if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
_indicatorController.showIndicator(
duration: Duration(minutes: 1),
color: Colors.red,
text: 'Disconnected',
);
} else if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.connecting) {
_indicatorController.showIndicator(
duration: Duration(minutes: 1),
color: Colors.yellow,
text: 'Reconnecting',
);
} else if (streamChat.client.wsConnectionStatus.value ==
ConnectionStatus.connected) {
_indicatorController.showIndicator(
duration: Duration(seconds: 5),
color: Colors.green,
text: 'Connected',
);
final streamChat = StreamChannel.of(context);
streamChat.queryMessages();
}
});
}
}
@@ -0,0 +1,353 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:date_format/date_format.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart';
import 'message_list_view.dart';
import 'stream_chat.dart';
import 'user_avatar.dart';
class MessageWidget extends StatefulWidget {
const MessageWidget({
Key key,
@required this.previousMessage,
@required this.message,
@required this.nextMessage,
this.onThreadSelect,
}) : super(key: key);
final Message previousMessage;
final Message message;
final Message nextMessage;
final OnThreadSelectCallback onThreadSelect;
@override
_MessageWidgetState createState() => _MessageWidgetState();
}
class _MessageWidgetState extends State<MessageWidget>
with AutomaticKeepAliveClientMixin {
final Map<String, ChangeNotifier> _videoControllers = {};
final Map<String, ChangeNotifier> _chuwieControllers = {};
@override
Widget build(BuildContext context) {
super.build(context);
final streamChat = StreamChat.of(context);
final currentUserId = streamChat.user.id;
final messageUserId = widget.message.user.id;
final previousUserId = widget.previousMessage?.user?.id;
final nextUserId = widget.nextMessage?.user?.id;
final isMyMessage = messageUserId == currentUserId;
final isLastUser = previousUserId == messageUserId;
final isNextUser = nextUserId == messageUserId;
final alignment =
isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
var row = <Widget>[
Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
_buildBubble(context, isMyMessage, isLastUser),
widget.message.replyCount > 0
? GestureDetector(
onTap: () {
if (widget.onThreadSelect != null) {
widget.onThreadSelect(widget.message);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
children: <Widget>[
Text(
'Replies: ${widget.message.replyCount}',
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(color: Colors.blue),
),
Icon(
Icons.subdirectory_arrow_left,
color: Colors.black12,
),
],
),
),
)
: Container(),
isNextUser ? Container() : _buildTimestamp(isMyMessage, alignment),
],
),
isNextUser
? Container(
width: 40,
)
: Padding(
padding: EdgeInsets.only(
left: isMyMessage ? 8.0 : 0,
right: isMyMessage ? 0 : 8.0,
),
child: UserAvatar(user: widget.message.user),
),
];
if (!isMyMessage) {
row = row.reversed.toList();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
margin: EdgeInsets.only(
top: isLastUser ? 5 : 24,
bottom: widget.nextMessage == null ? 30 : 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: row,
),
);
}
Widget _buildBubble(
BuildContext context,
bool isMyMessage,
bool isLastUser,
) {
var nOfAttachmentWidgets = 0;
final column = Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: widget.message.attachments.map((attachment) {
nOfAttachmentWidgets++;
Widget attachmentWidget;
if (attachment.type == 'video') {
attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser);
} else if (attachment.type == 'image' || attachment.type == 'giphy') {
attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment);
}
if (attachmentWidget != null) {
final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser)
.copyWith(color: Color(0xffebebeb));
return ClipRRect(
borderRadius: boxDecoration.borderRadius,
child: Container(
decoration: boxDecoration,
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: Stack(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
attachmentWidget,
attachment.title != null
? Container(
constraints:
BoxConstraints.loose(Size.fromHeight(70)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(color: Colors.blue),
),
Text(
Uri.parse(attachment.thumbUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.caption,
),
],
),
),
color: Color(0xffebebeb),
)
: Container(),
],
),
attachment.type == 'image' && attachment.titleLink != null
? Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launchURL(attachment.titleLink),
),
),
)
: SizedBox.fromSize(
size: Size.zero,
),
],
),
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 1 ? 5 : 0,
),
),
);
}
nOfAttachmentWidgets--;
return Container();
}).toList(),
);
if (widget.message.text.trim().isNotEmpty) {
column.children.add(Container(
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 0 ? 5 : 0,
),
decoration: _buildBoxDecoration(
isMyMessage, isLastUser || nOfAttachmentWidgets > 0),
padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody(
data: '${widget.message.text}',
onTapLink: (link) {
_launchURL(link);
},
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
),
));
}
return column;
}
Widget _buildImage(
bool isMyMessage,
bool isLastUser,
Attachment attachment,
) {
return CachedNetworkImage(
imageUrl: attachment.thumbUrl ?? attachment.imageUrl,
fit: BoxFit.cover,
);
}
Widget _buildVideo(
Attachment attachment,
bool isMyMessage,
bool isLastUser,
) {
VideoPlayerController videoController;
if (_videoControllers.containsKey(attachment.assetUrl)) {
videoController = _videoControllers[attachment.assetUrl];
} else {
videoController = VideoPlayerController.network(attachment.assetUrl);
_videoControllers[attachment.assetUrl] = videoController;
}
ChewieController chewieController;
if (_chuwieControllers.containsKey(attachment.assetUrl)) {
chewieController = _chuwieControllers[attachment.assetUrl];
} else {
chewieController = ChewieController(
videoPlayerController: videoController,
autoInitialize: true,
errorBuilder: (_, e) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
attachment.thumbUrl,
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launchURL(attachment.titleLink),
),
),
],
);
});
_chuwieControllers[attachment.assetUrl] = chewieController;
}
return Chewie(
key: ValueKey<String>(
'ATTACHMENT-${attachment.title}-${widget.message.id}'),
controller: chewieController,
);
}
Future<void> _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Cannot launch the url'),
),
);
}
}
@override
void dispose() {
_videoControllers.values.forEach((element) {
element.dispose();
});
super.dispose();
}
Widget _buildTimestamp(bool isMyMessage, Alignment alignment) {
return Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]),
),
);
}
BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) {
return BoxDecoration(
border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)),
borderRadius: BorderRadius.only(
topLeft: Radius.circular((isMyMessage || !isLastUser) ? 16 : 2),
bottomLeft: Radius.circular(isMyMessage ? 16 : 2),
topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16),
bottomRight: Radius.circular(isMyMessage ? 2 : 16),
),
color: isMyMessage ? Color(0xffebebeb) : Colors.white,
);
}
@override
bool get wantKeepAlive {
return widget.message.attachments.isNotEmpty;
}
}
@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
class StreamChannel extends StatefulWidget {
StreamChannel({
Key key,
@required this.child,
@required this.channelClient,
}) : super(
key: key,
);
final Widget child;
final Channel channelClient;
static StreamChannelState of(BuildContext context) {
StreamChannelState streamChannelState;
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
}
return streamChannelState;
}
@override
StreamChannelState createState() => StreamChannelState();
}
class StreamChannelState extends State<StreamChannel> {
StreamChannelState();
Channel get channelClient => widget.channelClient;
Channel get channel => widget.channelClient;
Stream<ChannelState> get channelStateStream =>
widget.channelClient.state.channelStateStream;
final BehaviorSubject<bool> _queryMessageController = BehaviorSubject();
Stream<bool> get queryMessage => _queryMessageController.stream;
void queryMessages() {
_queryMessageController.add(true);
String firstId;
if (channel.state.messages.isNotEmpty) {
firstId = channel.state.messages.first.id;
}
widget.channelClient
.query(
messagesPagination: PaginationParams(
lessThan: firstId,
limit: 100,
),
)
.then((res) {
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
});
}
Future<void> getReplies(String parentId) async {
_queryMessageController.add(true);
String firstId;
if (widget.channelClient.state.threads.containsKey(parentId)) {
firstId = widget.channelClient.state.threads[parentId].first.id;
}
return widget.channelClient
.getReplies(
parentId,
PaginationParams(
lessThan: firstId,
limit: 100,
),
)
.then((res) {
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
});
}
@override
void dispose() {
_queryMessageController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.channelClient == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return FutureBuilder<bool>(
future: widget.channelClient.initialized,
initialData: widget.channelClient.state != null,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} else if (snapshot.hasError) {
return Center(
child: Text(snapshot.error),
);
} else {
return widget.child;
}
},
);
}
}
@@ -0,0 +1,95 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
class StreamChat extends InheritedWidget {
final Client client;
final List<StreamSubscription> _subscriptions = [];
StreamChat({
Key key,
@required this.client,
@required Widget child,
}) : super(
key: key,
child: child,
) {
_subscriptions.add(client.on(EventType.messageNew).listen((Event e) {
final index = channels.indexWhere((c) => c.cid == e.cid);
if (index > 0) {
final channel = channels.removeAt(index);
channels.insert(0, channel);
}
}));
}
User get user => client.state.user;
Stream<User> get userStream => client.state.userStream;
final List<Channel> channels = [];
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
}) async {
if (_queryChannelsLoadingController.value) {
return;
}
_queryChannelsLoadingController.sink.add(true);
try {
client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
);
} finally {
_queryChannelsLoadingController.sink.add(false);
}
}
void clearChannels() {
channels.clear();
}
void dispose() {
client.dispose();
_subscriptions.forEach((s) => s.cancel());
_queryChannelsLoadingController.close();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
static StreamChat of(BuildContext context, [bool listen = false]) {
StreamChat streamChat;
if (listen) {
streamChat = context.dependOnInheritedWidgetOfExactType<StreamChat>();
} else {
streamChat = context.findAncestorWidgetOfExactType<StreamChat>();
}
if (streamChat == null) {
throw Exception(
'You must have a StreamChat widget at the top of your widget tree');
}
return streamChat;
}
}
@@ -0,0 +1,29 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
class UserAvatar extends StatelessWidget {
const UserAvatar({
Key key,
@required this.user,
this.radius = 16,
}) : super(key: key);
final User user;
final double radius;
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: this.radius,
backgroundImage: user.extraData.containsKey('image')
? CachedNetworkImageProvider(user.extraData['image'] as String)
: null,
child: user.extraData.containsKey('image')
? null
: Text(user?.extraData?.containsKey('name') ?? false
? user.extraData['name'][0]
: ''),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
name: stream_chat_example
description: example Flutter project using Stream Chat service
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_widgets: ^0.1.11
timeago: ^2.0.26
jiffy: ^3.0.1
date_format: ^1.0.8
cached_network_image: ^2.0.0
flutter_markdown: ^0.3.4
url_launcher: ^5.4.2
video_player: ^0.10.8+1
chewie: ^0.9.10
animations: ^1.0.0+5
stream_chat:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: any
build_web_compilers: any
flutter:
uses-material-design: true
@@ -0,0 +1,29 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="stream_chat">
<link rel="apple-touch-icon" href="/icons/Icon-192.png">
<title>stream_chat</title>
<link rel="manifest" href="/manifest.json">
</head>
<body>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
@@ -0,0 +1,23 @@
{
"name": "stream_chat",
"short_name": "stream_chat",
"start_url": ".",
"display": "minimal-ui",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
/// Used to notify the WS connection status
enum ConnectionStatus {
/// WS is connected and everything is good
connected,
/// WS is connecting (usually reconnecting)
connecting,
/// WS is disconnected and it's not reconnecting
disconnected,
}
@@ -0,0 +1,97 @@
import 'package:json_annotation/json_annotation.dart';
part 'requests.g.dart';
/// Sorting options
@JsonSerializable(createFactory: false)
class SortOption {
/// Ascending order
static const ASC = 1;
/// Descending order
static const DESC = -1;
/// A sorting field name
final String field;
/// A sorting direction
final int direction;
/// Creates a new SortOption instance
///
/// For example:
/// ```dart
/// // Sort channels by the last message date:
/// final sorting = SortOption("last_message_at")
/// ```
const SortOption(this.field, {this.direction = DESC});
/// Serialize model to json
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
}
/// Pagination options.
@JsonSerializable(createFactory: false, includeIfNull: false)
class PaginationParams {
/// The amount of items requested from the APIs.
final int limit;
/// The offset of requesting items.
final int offset;
/// Filter on ids greater than the given value.
@JsonKey(name: 'id_gt')
final String greaterThan;
/// Filter on ids greater than or equal to the given value.
@JsonKey(name: 'id_gte')
final String greaterThanOrEqual;
/// Filter on ids smaller than the given value.
@JsonKey(name: 'id_lt')
final String lessThan;
/// Filter on ids smaller than or equal to the given value.
@JsonKey(name: 'id_lte')
final String lessThanOrEqual;
/// Creates a new PaginationParams instance
///
/// For example:
/// ```dart
/// // limit to 50
/// final paginationParams = PaginationParams(limit: 50);
///
/// // limit to 50 with offset
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
/// ```
const PaginationParams({
this.limit = 10,
this.offset,
this.greaterThan,
this.greaterThanOrEqual,
this.lessThan,
this.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,
}) =>
PaginationParams(
limit: limit ?? this.limit,
offset: offset ?? this.offset,
greaterThan: greaterThan ?? this.greaterThan,
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
lessThan: lessThan ?? this.lessThan,
lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual,
);
}
@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'requests.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Map<String, dynamic> _$SortOptionToJson(SortOption instance) =>
<String, dynamic>{
'field': instance.field,
'direction': instance.direction,
};
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('limit', instance.limit);
writeNotNull('offset', instance.offset);
writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan);
writeNotNull('id_lte', instance.lessThanOrEqual);
return val;
}
@@ -0,0 +1,375 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/models/device.dart';
import 'package:stream_chat/src/models/event.dart';
import '../models/channel_model.dart';
import '../models/channel_state.dart';
import '../models/member.dart';
import '../models/message.dart';
import '../models/reaction.dart';
import '../models/read.dart';
import '../models/user.dart';
part 'responses.g.dart';
class _BaseResponse {
String duration;
}
/// Model response for [Client.resync] api call
@JsonSerializable(createToJson: false)
class SyncResponse extends _BaseResponse {
/// The list of events
List<Event> events;
/// Create a new instance from a json
static SyncResponse fromJson(Map<String, dynamic> json) =>
_$SyncResponseFromJson(json);
}
/// Model response for [Client.queryChannels] api call
@JsonSerializable(createToJson: false)
class QueryChannelsResponse extends _BaseResponse {
/// List of channels state returned by the query
List<ChannelState> channels;
/// Create a new instance from a json
static QueryChannelsResponse fromJson(Map<String, dynamic> json) =>
_$QueryChannelsResponseFromJson(json);
}
/// Model response for [Client.queryChannels] api call
@JsonSerializable(createToJson: false)
class TranslateMessageResponse extends _BaseResponse {
/// List of channels state returned by the query
TranslatedMessage message;
/// Create a new instance from a json
static TranslateMessageResponse fromJson(Map<String, dynamic> json) =>
_$TranslateMessageResponseFromJson(json);
}
/// Model response for [Client.queryChannels] api call
@JsonSerializable(createToJson: false)
class QueryMembersResponse extends _BaseResponse {
/// List of channels state returned by the query
List<Member> members;
/// Create a new instance from a json
static QueryMembersResponse fromJson(Map<String, dynamic> json) =>
_$QueryMembersResponseFromJson(json);
}
/// Model response for [Client.queryUsers] api call
@JsonSerializable(createToJson: false)
class QueryUsersResponse extends _BaseResponse {
/// List of users returned by the query
List<User> users;
/// Create a new instance from a json
static QueryUsersResponse fromJson(Map<String, dynamic> json) =>
_$QueryUsersResponseFromJson(json);
}
/// Model response for [channel.getReactions] api call
@JsonSerializable(createToJson: false)
class QueryReactionsResponse extends _BaseResponse {
/// List of reactions returned by the query
List<Reaction> reactions;
/// Create a new instance from a json
static QueryReactionsResponse fromJson(Map<String, dynamic> json) =>
_$QueryReactionsResponseFromJson(json);
}
/// Model response for [Channel.getReplies] api call
@JsonSerializable(createToJson: false)
class QueryRepliesResponse extends _BaseResponse {
/// List of messages returned by the api call
List<Message> messages;
/// Create a new instance from a json
static QueryRepliesResponse fromJson(Map<String, dynamic> json) =>
_$QueryRepliesResponseFromJson(json);
}
/// Model response for [Client.getDevices] api call
@JsonSerializable(createToJson: false)
class ListDevicesResponse extends _BaseResponse {
/// List of user devices
List<Device> devices;
/// Create a new instance from a json
static ListDevicesResponse fromJson(Map<String, dynamic> json) =>
_$ListDevicesResponseFromJson(json);
}
/// Model response for [Channel.sendFile] api call
@JsonSerializable(createToJson: false)
class SendFileResponse extends _BaseResponse {
/// The url of the uploaded file
String file;
/// Create a new instance from a json
static SendFileResponse fromJson(Map<String, dynamic> json) =>
_$SendFileResponseFromJson(json);
}
/// Model response for [Channel.sendImage] api call
@JsonSerializable(createToJson: false)
class SendImageResponse extends _BaseResponse {
/// The url of the uploaded file
String file;
/// Create a new instance from a json
static SendImageResponse fromJson(Map<String, dynamic> json) =>
_$SendImageResponseFromJson(json);
}
/// Model response for [Channel.sendReaction] api call
@JsonSerializable(createToJson: false)
class SendReactionResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
/// The reaction created by the api call
Reaction reaction;
/// Create a new instance from a json
static SendReactionResponse fromJson(Map<String, dynamic> json) =>
_$SendReactionResponseFromJson(json);
}
/// Model response for [Client.setGuestUser] api call
@JsonSerializable(createToJson: false)
class SetGuestUserResponse extends _BaseResponse {
/// Guest user access token
String accessToken;
/// Guest user
User user;
/// Create a new instance from a json
static SetGuestUserResponse fromJson(Map<String, dynamic> json) =>
_$SetGuestUserResponseFromJson(json);
}
/// Model response for [Client.updateUser] api call
@JsonSerializable(createToJson: false)
class UpdateUsersResponse extends _BaseResponse {
/// Updated users
Map<String, User> users;
/// Create a new instance from a json
static UpdateUsersResponse fromJson(Map<String, dynamic> json) =>
_$UpdateUsersResponseFromJson(json);
}
/// Model response for [Client.updateMessage] api call
@JsonSerializable(createToJson: false)
class UpdateMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static UpdateMessageResponse fromJson(Map<String, dynamic> json) =>
_$UpdateMessageResponseFromJson(json);
}
/// Model response for [Channel.sendMessage] api call
@JsonSerializable(createToJson: false)
class SendMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static SendMessageResponse fromJson(Map<String, dynamic> json) =>
_$SendMessageResponseFromJson(json);
}
/// Model response for [Client.getMessage] api call
@JsonSerializable(createToJson: false)
class GetMessageResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
/// Channel of the message
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');
if (jsonChannel != null) {
res.channel = ChannelModel.fromJson(jsonChannel);
}
return res;
}
}
/// Model response for [Client.search] api call
@JsonSerializable(createToJson: false)
class SearchMessagesResponse extends _BaseResponse {
/// List of messages returned by the api call
List<GetMessageResponse> results;
/// Create a new instance from a json
static SearchMessagesResponse fromJson(Map<String, dynamic> json) =>
_$SearchMessagesResponseFromJson(json);
}
/// Model response for [Channel.getMessagesById] api call
@JsonSerializable(createToJson: false)
class GetMessagesByIdResponse extends _BaseResponse {
/// Message returned by the api call
List<Message> messages;
/// Create a new instance from a json
static GetMessagesByIdResponse fromJson(Map<String, dynamic> json) =>
_$GetMessagesByIdResponseFromJson(json);
}
/// Model response for [Channel.update] api call
@JsonSerializable(createToJson: false)
class UpdateChannelResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static UpdateChannelResponse fromJson(Map<String, dynamic> json) =>
_$UpdateChannelResponseFromJson(json);
}
/// Model response for [Channel.inviteMembers] api call
@JsonSerializable(createToJson: false)
class InviteMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static InviteMembersResponse fromJson(Map<String, dynamic> json) =>
_$InviteMembersResponseFromJson(json);
}
/// Model response for [Channel.removeMembers] api call
@JsonSerializable(createToJson: false)
class RemoveMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static RemoveMembersResponse fromJson(Map<String, dynamic> json) =>
_$RemoveMembersResponseFromJson(json);
}
/// Model response for [Channel.sendAction] api call
@JsonSerializable(createToJson: false)
class SendActionResponse extends _BaseResponse {
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static SendActionResponse fromJson(Map<String, dynamic> json) =>
_$SendActionResponseFromJson(json);
}
/// Model response for [Channel.addMembers] api call
@JsonSerializable(createToJson: false)
class AddMembersResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static AddMembersResponse fromJson(Map<String, dynamic> json) =>
_$AddMembersResponseFromJson(json);
}
/// Model response for [Channel.acceptInvite] api call
@JsonSerializable(createToJson: false)
class AcceptInviteResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static AcceptInviteResponse fromJson(Map<String, dynamic> json) =>
_$AcceptInviteResponseFromJson(json);
}
/// Model response for [Channel.rejectInvite] api call
@JsonSerializable(createToJson: false)
class RejectInviteResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// Channel members
List<Member> members;
/// Message returned by the api call
Message message;
/// Create a new instance from a json
static RejectInviteResponse fromJson(Map<String, dynamic> json) =>
_$RejectInviteResponseFromJson(json);
}
/// Model response for empty responses
@JsonSerializable(createToJson: false)
class EmptyResponse extends _BaseResponse {
/// Create a new instance from a json
static EmptyResponse fromJson(Map<String, dynamic> json) =>
_$EmptyResponseFromJson(json);
}
/// Model response for [Channel.query] api call
@JsonSerializable(createToJson: false)
class ChannelStateResponse extends _BaseResponse {
/// Updated channel
ChannelModel channel;
/// List of messages returned by the api call
List<Message> messages;
/// Channel members
List<Member> members;
/// Number of users watching the channel
int watcherCount;
/// List of read states
List<Read> read;
/// Create a new instance from a json
static ChannelStateResponse fromJson(Map<String, dynamic> json) =>
_$ChannelStateResponseFromJson(json);
}
@@ -0,0 +1,382 @@
// 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),
));
}
SetGuestUserResponse _$SetGuestUserResponseFromJson(Map json) {
return SetGuestUserResponse()
..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),
));
}
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();
}
@@ -0,0 +1,37 @@
import 'package:flutter/foundation.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(Client client, int attempt, ApiError apiError)
shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
final Duration Function(Client client, int attempt, ApiError apiError)
retryTimeout;
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
RetryPolicy copyWith({
bool Function(Client client, int attempt, ApiError apiError) shouldRetry,
Duration Function(Client client, int attempt, ApiError apiError)
retryTimeout,
int attempt,
}) =>
RetryPolicy(
retryTimeout: retryTimeout ?? this.retryTimeout,
shouldRetry: shouldRetry ?? this.shouldRetry,
attempt: attempt ?? this.attempt,
);
}

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