Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into docusaurus
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BRANCH=$1
|
||||
|
||||
if [ "$BRANCH" == "dev" ]
|
||||
then
|
||||
# TODO Flutter dev branch is currently broken so we're unable to test MacOS.
|
||||
echo "TODO: Skipping macOS testing due to Flutter dev branch issue. Switching branch to stable."
|
||||
BRANCH=stable
|
||||
fi
|
||||
|
||||
git clone https://github.com/flutter/flutter.git --depth 1 -b $BRANCH _flutter
|
||||
echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin"
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
flutter pub global activate melos
|
||||
echo "::add-path::$HOME/.pub-cache/bin"
|
||||
echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin"
|
||||
echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin"
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
# Fast fail the script on failures.
|
||||
set -e
|
||||
|
||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$'
|
||||
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$'
|
||||
@@ -2,6 +2,7 @@ name: stream_flutter_workflow
|
||||
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||
flutter_version: "2.2.2"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -9,97 +10,114 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: "Git Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Install Flutter'
|
||||
run: ./.github/workflows/scripts/install-flutter.sh stable
|
||||
- name: 'Install Tools'
|
||||
- name: Cache Flutter dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /opt/hostedtoolcache/flutter
|
||||
key: ${{ env.flutter_version }}-flutter
|
||||
- name: "Install Flutter"
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: |
|
||||
./.github/workflows/scripts/install-tools.sh
|
||||
flutter pub global activate tuneup
|
||||
- name: 'Bootstrap Workspace'
|
||||
run: melos bootstrap --verbose
|
||||
- name: 'Dart Analyze'
|
||||
flutter pub global activate melos
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
- name: "Dart Analyze"
|
||||
run: |
|
||||
melos exec -c 3 --ignore="*example*" -- \
|
||||
tuneup check
|
||||
- name: 'Pub Check'
|
||||
melos run analyze
|
||||
- name: "Pub Check"
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
melos exec -c 1 --no-private --ignore="*example*" -- \
|
||||
pub publish --dry-run
|
||||
melos run lint:pub
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: "Git Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Install Flutter'
|
||||
run: ./.github/workflows/scripts/install-flutter.sh stable
|
||||
- name: 'Install Tools'
|
||||
- name: Cache Flutter dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /opt/hostedtoolcache/flutter
|
||||
key: ${{ env.flutter_version }}-flutter
|
||||
- name: "Install Flutter"
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: flutter pub global activate melos
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
- name: "Melos Format"
|
||||
run: melos run format
|
||||
- name: "Validate Formatting"
|
||||
run: |
|
||||
./.github/workflows/scripts/install-tools.sh
|
||||
- name: 'Bootstrap Workspace'
|
||||
run: melos bootstrap --verbose
|
||||
- name: 'Dart'
|
||||
run: |
|
||||
melos exec -c 1 -- \
|
||||
flutter format .
|
||||
./.github/workflows/scripts/validate-formatting.sh
|
||||
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: "Git Checkout"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Install Flutter'
|
||||
run: ./.github/workflows/scripts/install-flutter.sh stable
|
||||
- name: 'Install Tools'
|
||||
- name: Cache Flutter dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /Users/runner/hostedtoolcache/flutter
|
||||
key: ${{ env.flutter_version }}-flutter
|
||||
- name: "Install Flutter"
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
- name: "Install Tools"
|
||||
run: |
|
||||
./.github/workflows/scripts/install-tools.sh
|
||||
flutter pub global activate coverage
|
||||
flutter pub global activate remove_from_coverage
|
||||
- name: 'Bootstrap Workspace'
|
||||
run: melos bootstrap --verbose
|
||||
- name: 'Dart Test'
|
||||
run: |
|
||||
cd packages/stream_chat
|
||||
flutter pub run test --coverage coverage/
|
||||
format_coverage --lcov --in=coverage/ --out=coverage/lcov.info --packages=.packages --report-on=lib
|
||||
- name: 'Flutter Test'
|
||||
run: |
|
||||
melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
|
||||
flutter test --coverage
|
||||
- name: CodeCov
|
||||
run: |
|
||||
melos exec -c 3 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
|
||||
"\$MELOS_ROOT_PATH/.github/workflows/scripts/coverage.sh"
|
||||
bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
|
||||
- uses: VeryGoodOpenSource/[email protected]
|
||||
flutter pub global activate melos
|
||||
pub global activate remove_from_coverage
|
||||
- name: "Bootstrap Workspace"
|
||||
run: melos bootstrap
|
||||
- name: "Flutter Test"
|
||||
run: melos run test:all
|
||||
- name: "Collect Coverage"
|
||||
run: melos run coverage:ignore-file --no-select
|
||||
- name: "Upload Coverage"
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
files: packages/*/coverage/lcov.info
|
||||
- name: "Stream Chat Coverage Check"
|
||||
uses: VeryGoodOpenSource/[email protected]
|
||||
with:
|
||||
path: packages/stream_chat/coverage/lcov.info
|
||||
min_coverage: 40
|
||||
- uses: VeryGoodOpenSource/[email protected]
|
||||
- name: "Stream Chat Persistence Coverage Check"
|
||||
uses: VeryGoodOpenSource/[email protected]
|
||||
with:
|
||||
path: packages/stream_chat_persistence/coverage/lcov.info
|
||||
min_coverage: 95
|
||||
- uses: VeryGoodOpenSource/[email protected]
|
||||
- name: "Stream Chat Flutter Core Coverage Check"
|
||||
uses: VeryGoodOpenSource/[email protected]
|
||||
with:
|
||||
path: packages/stream_chat_flutter_core/coverage/lcov.info
|
||||
min_coverage: 90
|
||||
- uses: VeryGoodOpenSource/[email protected]
|
||||
- name: "Stream Chat Flutter Coverage Check"
|
||||
uses: VeryGoodOpenSource/[email protected]
|
||||
with:
|
||||
path: packages/stream_chat_flutter/coverage/lcov.info
|
||||
min_coverage: 35
|
||||
min_coverage: 35
|
||||
@@ -1,12 +1,10 @@
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- extension-methods
|
||||
exclude:
|
||||
- lib/**/*.g.dart
|
||||
- example/**
|
||||
- lib/src/emoji
|
||||
- lib/**/*.freezed.dart
|
||||
- test/**
|
||||
- packages/*/lib/**/*.g.dart
|
||||
- packages/*/example/**
|
||||
- packages/*/lib/src/emoji
|
||||
- packages/*/lib/**/*.freezed.dart
|
||||
- packages/*/test/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
@@ -147,4 +145,4 @@ linter:
|
||||
- cast_nullable_to_non_nullable
|
||||
- unnecessary_null_checks
|
||||
- tighten_type_of_initializing_formals
|
||||
- null_check_on_nullable_type_parameter
|
||||
- null_check_on_nullable_type_parameter
|
||||
+54
-42
@@ -1,4 +1,4 @@
|
||||
name: stream_chat_dart
|
||||
name: stream_chat_flutter
|
||||
|
||||
versioning:
|
||||
mode: independent
|
||||
@@ -7,56 +7,68 @@ packages:
|
||||
- packages/**
|
||||
|
||||
scripts:
|
||||
lint:all:
|
||||
run: melos run analyze && melos run format
|
||||
description: Run all static analysis checks
|
||||
|
||||
# - Requires `pub global activate tuneup`.
|
||||
analyze: >
|
||||
melos exec -c 1 --fail-fast -- \
|
||||
pub global run tuneup check
|
||||
analyze:
|
||||
run: |
|
||||
melos exec -c 4 --ignore="*example*" -- \
|
||||
dart analyze --fatal-infos .
|
||||
description: |
|
||||
Run `dart analyze` in all packages.
|
||||
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
|
||||
|
||||
format: pub global run flutter_plugin_tools format
|
||||
format:
|
||||
run: flutter format --set-exit-if-changed .
|
||||
description: |
|
||||
Run `flutter format --set-exit-if-changed .` in all packages.
|
||||
|
||||
lint:pub:
|
||||
run: |
|
||||
melos exec -c 4 --no-private --ignore="*example*" -- \
|
||||
pub publish --dry-run
|
||||
description: |
|
||||
Run `pub publish --dry-run` in all packages.
|
||||
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
|
||||
|
||||
build:examples:ios: >
|
||||
melos exec -c 1 --scope="*example*" --fail-fast -- \
|
||||
flutter build ios --no-codesign
|
||||
generate:all:
|
||||
run: melos run generate:dart && melos run generate:flutter
|
||||
description: Build all generated files for Dart & Flutter packages in this project.
|
||||
|
||||
generate:dart:
|
||||
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs"
|
||||
description: Build all generated files for Dart packages in this project.
|
||||
|
||||
build:examples:android: >
|
||||
melos exec -c 1 --scope="*example*" --fail-fast -- \
|
||||
flutter build apk
|
||||
generate:flutter:
|
||||
run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs"
|
||||
description: Build all generated files for Flutter packages in this project.
|
||||
|
||||
# Build any plugin example apps that have MacOS support.
|
||||
# - Requires `flutter config --enable-macos-desktop` enabled.
|
||||
# - Requires `flutter channel master && flutter upgrade`.
|
||||
build:examples:macos: >
|
||||
melos exec -c 1 --scope="*example*" --dir-exists=macos --fail-fast -- \
|
||||
flutter build macos
|
||||
test:all:
|
||||
run: melos run test:dart --no-select && melos run test:flutter --no-select
|
||||
description: Run all Dart & Flutter tests in this project.
|
||||
|
||||
test:dart:
|
||||
run: melos exec -c 1 --fail-fast -- "flutter test --coverage"
|
||||
description: Run Dart tests for a specific package in this project.
|
||||
select-package:
|
||||
flutter: false
|
||||
dir-exists: test
|
||||
|
||||
test:dart: >
|
||||
melos exec -c 1 --fail-fast --no-flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
|
||||
flutter pub run test
|
||||
test:flutter:
|
||||
run: melos exec -c 3 --fail-fast -- "flutter test --coverage"
|
||||
description: Run Flutter tests for a specific package in this project.
|
||||
select-package:
|
||||
flutter: true
|
||||
dir-exists: test
|
||||
|
||||
test:flutter: >
|
||||
melos exec -c 1 --fail-fast --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
|
||||
flutter test
|
||||
|
||||
test:web: >
|
||||
melos exec -c 1 --fail-fast --dir-exists=test --scope="*web*" -- \
|
||||
flutter test --platform=chrome
|
||||
|
||||
|
||||
lint:pub: >
|
||||
melos exec -c 5 --fail-fast --no-private --ignore="*example*" -- \
|
||||
pub publish --dry-run
|
||||
|
||||
|
||||
postclean: >
|
||||
melos exec -- \
|
||||
rm -rf ./build ./android/.gradle ./ios/.symlinks ./ios/Pods ./android/.idea ./.idea ./.dart-tool/build
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: 1.9.2
|
||||
coverage:ignore-file:
|
||||
run: |
|
||||
melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh"
|
||||
description: Removes all the ignored files from the coverage report.
|
||||
select-package:
|
||||
dir-exists: coverage
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
flutter: '>=1.22.4 <2.0.0'
|
||||
@@ -1,3 +1,54 @@
|
||||
## 2.0.0-nullsafety.7
|
||||
|
||||
🛑️ Breaking Changes from `2.0.0-nullsafety.6`
|
||||
|
||||
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor)
|
||||
- `client.disconnect()` is now divided into two different functions
|
||||
- `client.closeConnection()` -> for closing user websocket connection.
|
||||
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
|
||||
- `client.devToken()` now returns a `Token` model instead of `String`.
|
||||
- `ApiError` is removed in favor of `StreamChatError`
|
||||
- `StreamChatError` -> parent type for all the stream errors.
|
||||
- `StreamWebSocketError` -> for user websocket related errors.
|
||||
- `StreamChatNetworkError` -> for network related errors.
|
||||
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
|
||||
- `option.state` -> bool state
|
||||
- `option.watch` -> bool watch
|
||||
- `option.presence` -> bool presence
|
||||
- `client.queryUsers()` options param is removed in favor of individual params
|
||||
- `option.presence` -> bool presence
|
||||
|
||||
✅ Added
|
||||
|
||||
- New `Location` enum is introduced for easily changing the client location/baseUrl.
|
||||
- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- `baseURL` is now deprecated in favor of using `Location` to change data location.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet
|
||||
connection
|
||||
|
||||
## 2.0.0-nullsafety.6
|
||||
|
||||
- Fix thread reply not working with attachments
|
||||
- Minor fixes
|
||||
|
||||
## 2.0.0-nullsafety.6
|
||||
|
||||
- Fix thread reply not working with attachments
|
||||
- Minor fixes
|
||||
|
||||
## 2.0.0-nullsafety.5
|
||||
|
||||
- Minor fixes
|
||||
- Performance improvements
|
||||
- Fixed `skip_push` in `client.sendMessage`
|
||||
- Added partial message update method
|
||||
|
||||
## 2.0.0-nullsafety.2
|
||||
|
||||
- Added new `Filter.raw` constructor
|
||||
@@ -45,7 +96,8 @@
|
||||
- Save pinned messages in offline storage
|
||||
- Minor fixes
|
||||
- `StreamClient.QueryChannels` now returns a Stream and fetches the channels from storage before calling the api
|
||||
- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or offline
|
||||
- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or
|
||||
offline
|
||||
|
||||
## 1.2.0-beta
|
||||
|
||||
@@ -56,7 +108,8 @@
|
||||
## 1.1.0-beta
|
||||
|
||||
- Fixed minor bugs
|
||||
- Add support for custom attachment upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart)
|
||||
- Add support for custom attachment
|
||||
upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart)
|
||||
- Add support for asynchronous attachment upload
|
||||
|
||||
## 1.0.3-beta
|
||||
@@ -66,7 +119,8 @@
|
||||
|
||||
## 1.0.2-beta
|
||||
|
||||
- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser`, `connectUserWithProvider`
|
||||
- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser`
|
||||
, `connectUserWithProvider`
|
||||
- Optimised reaction updates - i.e., Update first call Api later.
|
||||
|
||||
## 1.0.1-beta
|
||||
@@ -76,9 +130,11 @@
|
||||
## 1.0.0-beta
|
||||
|
||||
- 🛑 **BREAKING** Renamed `Client` to less generic `StreamChatClient`
|
||||
- 🛑 **BREAKING** Segregated the persistence layer into separate package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence)
|
||||
- 🛑 **BREAKING** Segregated the persistence layer into separate
|
||||
package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence)
|
||||
- 🛑 **BREAKING** Moved `Client.backgroundKeepAlive` to [core package](https://pub.dev/packages/stream_chat_core)
|
||||
- 🛑 **BREAKING** Moved `Client.showLocalNotification` to [core package](https://pub.dev/packages/stream_chat_core) and renamed it to `StreamChatCore.onBackgroundEventReceived`
|
||||
- 🛑 **BREAKING** Moved `Client.showLocalNotification` to [core package](https://pub.dev/packages/stream_chat_core) and
|
||||
renamed it to `StreamChatCore.onBackgroundEventReceived`
|
||||
- Removed `flutter` dependency. This is now a pure Dart package 🥳
|
||||
- Minor improvements and bugfixes
|
||||
|
||||
@@ -133,7 +189,8 @@
|
||||
|
||||
## 0.2.20
|
||||
|
||||
- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we cannot sort by custom properties.
|
||||
- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we
|
||||
cannot sort by custom properties.
|
||||
|
||||
## 0.2.19
|
||||
|
||||
@@ -147,7 +204,7 @@
|
||||
|
||||
## 0.2.17+1
|
||||
|
||||
- Do not retry messages when server returns error
|
||||
- Do not retry messages when server returns error
|
||||
|
||||
## 0.2.17
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- extension-methods
|
||||
exclude:
|
||||
- lib/**/*.g.dart
|
||||
- example/**
|
||||
- lib/src/emoji
|
||||
- lib/**/*.freezed.dart
|
||||
- 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_use_package_imports
|
||||
- avoid_empty_else
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_slow_async_io
|
||||
- avoid_types_as_parameter_names
|
||||
- cancel_subscriptions
|
||||
- close_sinks
|
||||
- control_flow_in_finally
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- list_remove_unrelated_type
|
||||
- literal_only_boolean_expressions
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
- no_logic_in_create_state
|
||||
- prefer_void_to_null
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
- unnecessary_statements
|
||||
- unrelated_type_equality_checks
|
||||
- omit_local_variable_types
|
||||
- use_key_in_widget_constructors
|
||||
- valid_regexps
|
||||
- always_declare_return_types
|
||||
- always_require_non_null_named_parameters
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_catching_errors
|
||||
- avoid_init_to_null
|
||||
- avoid_null_checks_in_equality_operators
|
||||
- avoid_positional_boolean_parameters
|
||||
- avoid_private_typedef_functions
|
||||
- avoid_redundant_argument_values
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_unused_constructor_parameters
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cascade_invocations
|
||||
|
||||
- constant_identifier_names
|
||||
- curly_braces_in_flow_control_structures
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- exhaustive_cases
|
||||
- file_names
|
||||
- implementation_imports
|
||||
- join_return_with_assignment
|
||||
- leading_newlines_in_multiline_strings
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- lines_longer_than_80_chars
|
||||
- missing_whitespace_between_adjacent_strings
|
||||
- non_constant_identifier_names
|
||||
- null_closures
|
||||
- one_member_abstracts
|
||||
- only_throw_errors
|
||||
- package_api_docs
|
||||
- package_prefixed_library_names
|
||||
- parameter_assignments
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_asserts_in_initializer_lists
|
||||
- prefer_asserts_with_message
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_constructors_over_static_methods
|
||||
- prefer_contains
|
||||
- prefer_equal_for_default_values
|
||||
- prefer_expression_function_bodies
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
- prefer_function_declarations_over_variables
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_if_elements_to_conditional_expressions
|
||||
- prefer_if_null_operators
|
||||
- prefer_initializing_formals
|
||||
- prefer_inlined_adds
|
||||
- prefer_int_literals
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_is_not_operator
|
||||
- prefer_null_aware_operators
|
||||
- prefer_single_quotes
|
||||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- provide_deprecation_message
|
||||
- public_member_api_docs
|
||||
- recursive_getters
|
||||
- sized_box_for_whitespace
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_constructors_first
|
||||
- sort_unnamed_constructors_first
|
||||
|
||||
- type_annotate_public_apis
|
||||
- type_init_formals
|
||||
- unnecessary_await_in_return
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_const
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_null_in_if_null_operators
|
||||
- unnecessary_nullable_for_final_variable_declarations
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_raw_strings
|
||||
- unnecessary_string_escapes
|
||||
- unnecessary_string_interpolations
|
||||
- unnecessary_this
|
||||
- use_is_even_rather_than_modulo
|
||||
- use_late_for_private_fields_and_variables
|
||||
- use_rethrow_when_possible
|
||||
- use_setters_to_change_properties
|
||||
- use_to_and_as_if_applicable
|
||||
- package_names
|
||||
- sort_pub_dependencies
|
||||
|
||||
- cast_nullable_to_non_nullable
|
||||
- unnecessary_null_checks
|
||||
- tighten_type_of_initializing_formals
|
||||
- null_check_on_nullable_type_parameter
|
||||
@@ -1,38 +0,0 @@
|
||||
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 = 0,
|
||||
});
|
||||
|
||||
/// The number of attempts tried so far
|
||||
int attempt = 0;
|
||||
|
||||
/// This function evaluates if we should retry the failure
|
||||
final bool Function(StreamChatClient client, int attempt, ApiError? apiError)
|
||||
shouldRetry;
|
||||
|
||||
/// In the case that we want to retry a failed request the retryTimeout
|
||||
/// method is called to determine the timeout
|
||||
final Duration Function(
|
||||
StreamChatClient client, int attempt, ApiError? apiError) retryTimeout;
|
||||
|
||||
/// Creates a copy of [RetryPolicy] with specified attributes overridden.
|
||||
RetryPolicy copyWith({
|
||||
bool Function(StreamChatClient client, int attempt, ApiError? apiError)?
|
||||
shouldRetry,
|
||||
Duration Function(StreamChatClient client, int attempt, ApiError? apiError)?
|
||||
retryTimeout,
|
||||
int? attempt,
|
||||
}) =>
|
||||
RetryPolicy(
|
||||
retryTimeout: retryTimeout ?? this.retryTimeout,
|
||||
shouldRetry: shouldRetry ?? this.shouldRetry,
|
||||
attempt: attempt ?? this.attempt,
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:stream_chat/src/api/channel.dart';
|
||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/exceptions.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// The retry queue associated to a channel
|
||||
class RetryQueue {
|
||||
/// Instantiate a new RetryQueue object
|
||||
RetryQueue({
|
||||
required this.channel,
|
||||
this.logger,
|
||||
}) {
|
||||
_retryPolicy = channel.client.retryPolicy;
|
||||
|
||||
_listenConnectionRecovered();
|
||||
|
||||
_listenFailedEvents();
|
||||
}
|
||||
|
||||
/// The channel of this queue
|
||||
final Channel channel;
|
||||
|
||||
/// The logger associated to this queue
|
||||
final Logger? logger;
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
void _listenConnectionRecovered() {
|
||||
_subscriptions
|
||||
.add(channel.client.on(EventType.connectionRecovered).listen((event) {
|
||||
if (!_isRetrying && event.online!) {
|
||||
_startRetrying();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
final HeapPriorityQueue<Message> _messageQueue = HeapPriorityQueue(_byDate);
|
||||
bool _isRetrying = false;
|
||||
RetryPolicy? _retryPolicy;
|
||||
|
||||
/// Add a list of messages
|
||||
void add(List<Message> messages) {
|
||||
logger?.info('added ${messages.length} messages');
|
||||
final messageList = _messageQueue.toList();
|
||||
|
||||
_messageQueue.addAll(messages
|
||||
.where((element) => !messageList.any((m) => m.id == element.id)));
|
||||
|
||||
if (_messageQueue.isNotEmpty && !_isRetrying) {
|
||||
_startRetrying();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startRetrying() async {
|
||||
logger?.info('start retrying');
|
||||
_isRetrying = true;
|
||||
final retryPolicy = _retryPolicy!.copyWith(attempt: 0);
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.first;
|
||||
try {
|
||||
logger?.info('retry attempt ${retryPolicy.attempt}');
|
||||
await _sendMessage(message);
|
||||
logger?.info('message sent - removing it from the queue');
|
||||
_messageQueue.remove(message);
|
||||
logger?.info('now ${_messageQueue.length} messages in the queue');
|
||||
retryPolicy.attempt = 0;
|
||||
} catch (error) {
|
||||
ApiError? apiError;
|
||||
if (error is DioError) {
|
||||
if (error.type == DioErrorType.response) {
|
||||
_messageQueue.remove(message);
|
||||
return;
|
||||
}
|
||||
apiError = ApiError(
|
||||
error.response?.data,
|
||||
error.response?.statusCode,
|
||||
);
|
||||
} else if (error is ApiError) {
|
||||
apiError = error;
|
||||
if (apiError.status?.toString().startsWith('4') == true) {
|
||||
_messageQueue.remove(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!retryPolicy.shouldRetry(
|
||||
channel.client,
|
||||
retryPolicy.attempt,
|
||||
apiError,
|
||||
)) {
|
||||
_messageQueue.toList().forEach(_sendFailedEvent);
|
||||
_isRetrying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
retryPolicy.attempt++;
|
||||
|
||||
final timeout = retryPolicy.retryTimeout(
|
||||
channel.client,
|
||||
retryPolicy.attempt,
|
||||
apiError,
|
||||
);
|
||||
await Future.delayed(timeout);
|
||||
}
|
||||
}
|
||||
_isRetrying = false;
|
||||
}
|
||||
|
||||
void _sendFailedEvent(Message? message) {
|
||||
final newStatus = message!.status == MessageSendingStatus.sending
|
||||
? MessageSendingStatus.failed
|
||||
: (message.status == MessageSendingStatus.updating
|
||||
? MessageSendingStatus.failed_update
|
||||
: MessageSendingStatus.failed_delete);
|
||||
channel.state!.addMessage(message.copyWith(
|
||||
status: newStatus,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _sendMessage(Message message) async {
|
||||
if (message.status == MessageSendingStatus.failed_update ||
|
||||
message.status == MessageSendingStatus.updating) {
|
||||
await channel.updateMessage(message);
|
||||
} else if (message.status == MessageSendingStatus.failed ||
|
||||
message.status == MessageSendingStatus.sending) {
|
||||
await channel.sendMessage(message);
|
||||
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||
message.status == MessageSendingStatus.deleting) {
|
||||
await channel.deleteMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenFailedEvents() {
|
||||
_subscriptions.add(channel.on().listen((event) {
|
||||
final messageList = _messageQueue.toList();
|
||||
if (event.message != null) {
|
||||
final messageIndex =
|
||||
messageList.indexWhere((m) => m.id == event.message!.id);
|
||||
if (messageIndex == -1 &&
|
||||
[
|
||||
MessageSendingStatus.failed_update,
|
||||
MessageSendingStatus.failed,
|
||||
MessageSendingStatus.failed_delete,
|
||||
].contains(event.message!.status)) {
|
||||
logger?.info('add message from events');
|
||||
final m = event.message;
|
||||
|
||||
if (m != null) {
|
||||
add([m]);
|
||||
}
|
||||
} else if (messageIndex != -1 &&
|
||||
[
|
||||
MessageSendingStatus.sent,
|
||||
null,
|
||||
].contains(event.message!.status)) {
|
||||
_messageQueue.remove(messageList[messageIndex]);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Call this method to dispose this object
|
||||
void dispose() {
|
||||
_messageQueue.clear();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
}
|
||||
|
||||
static int _byDate(Message m1, Message m2) {
|
||||
final date1 = _getMessageDate(m1);
|
||||
final date2 = _getMessageDate(m2);
|
||||
|
||||
if (date1 == null || date2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return date1.compareTo(date2);
|
||||
}
|
||||
|
||||
static DateTime? _getMessageDate(Message m1) {
|
||||
switch (m1.status) {
|
||||
case MessageSendingStatus.failed_delete:
|
||||
case MessageSendingStatus.deleting:
|
||||
return m1.deletedAt;
|
||||
|
||||
case MessageSendingStatus.failed:
|
||||
case MessageSendingStatus.sending:
|
||||
return m1.createdAt;
|
||||
|
||||
case MessageSendingStatus.failed_update:
|
||||
case MessageSendingStatus.updating:
|
||||
return m1.updatedAt;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import 'package:web_socket_channel/html.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Html version of websocket implementation
|
||||
/// Used in Flutter web version
|
||||
WebSocketChannel connectWebSocket(String url, {Iterable<String>? protocols}) =>
|
||||
HtmlWebSocketChannel.connect(url, protocols: protocols);
|
||||
@@ -1,7 +0,0 @@
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// IO version of websocket implementation
|
||||
/// Used in Flutter mobile version
|
||||
WebSocketChannel connectWebSocket(String url, {Iterable<String>? protocols}) =>
|
||||
IOWebSocketChannel.connect(url, protocols: protocols);
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Stub version of websocket implementation
|
||||
/// Used just for conditional library import
|
||||
WebSocketChannel connectWebSocket(String url,
|
||||
{Iterable<String>? protocols,
|
||||
Map<String, dynamic>? headers,
|
||||
Duration? pingInterval}) =>
|
||||
throw UnimplementedError();
|
||||
@@ -1,321 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/api/connection_status.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Typedef which exposes an [Event] as the only parameter.
|
||||
typedef EventHandler = void Function(Event);
|
||||
|
||||
/// Typedef used for connecting to a websocket. Method returns a
|
||||
/// [WebSocketChannel] and accepts a connection [url] and an optional
|
||||
/// [Iterable] of `protocols`.
|
||||
typedef ConnectWebSocket = WebSocketChannel Function(String? url,
|
||||
{Iterable<String>? protocols});
|
||||
|
||||
// TODO: parse error even
|
||||
// TODO: if parsing an error into an event fails we should not hide the
|
||||
// TODO: original error
|
||||
/// A WebSocket connection that reconnects upon failure.
|
||||
class WebSocket {
|
||||
/// Creates a new websocket
|
||||
/// To connect the WS call [connect]
|
||||
WebSocket({
|
||||
required this.baseUrl,
|
||||
required this.user,
|
||||
required this.handler,
|
||||
this.connectParams = const {},
|
||||
this.connectPayload = const {},
|
||||
this.logger,
|
||||
this.connectFunc,
|
||||
this.reconnectionMonitorInterval = 1,
|
||||
this.healthCheckInterval = 20,
|
||||
this.reconnectionMonitorTimeout = 40,
|
||||
}) {
|
||||
final qs = Map<String, String>.from(connectParams);
|
||||
|
||||
final data = Map<String, dynamic>.from(connectPayload);
|
||||
|
||||
data['user_details'] = user.toJson();
|
||||
qs['json'] = json.encode(data);
|
||||
|
||||
if (baseUrl.startsWith('https')) {
|
||||
_path = baseUrl.replaceFirst('https://', '');
|
||||
_path = Uri.https(_path, 'connect', qs)
|
||||
.toString()
|
||||
.replaceFirst('https', 'wss');
|
||||
} else if (baseUrl.startsWith('http')) {
|
||||
_path = baseUrl.replaceFirst('http://', '');
|
||||
_path =
|
||||
Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws');
|
||||
} else {
|
||||
_path = Uri.https(baseUrl, 'connect', qs)
|
||||
.toString()
|
||||
.replaceFirst('https', 'wss');
|
||||
}
|
||||
}
|
||||
|
||||
/// WS base url
|
||||
final String baseUrl;
|
||||
|
||||
/// User performing the WS connection
|
||||
final User user;
|
||||
|
||||
/// Querystring connection parameters
|
||||
final Map<String, String> connectParams;
|
||||
|
||||
/// WS connection payload
|
||||
final Map<String, dynamic> connectPayload;
|
||||
|
||||
/// Functions that will be called every time a new event is received from the
|
||||
/// connection
|
||||
final EventHandler handler;
|
||||
|
||||
/// A WS specific logger instance
|
||||
final Logger? logger;
|
||||
|
||||
/// Connection function
|
||||
/// Used only for testing purpose
|
||||
@visibleForTesting
|
||||
final ConnectWebSocket? connectFunc;
|
||||
|
||||
/// Interval of the reconnection monitor timer
|
||||
/// This checks that it received a new event in the last
|
||||
/// [reconnectionMonitorTimeout] seconds, otherwise it considers the
|
||||
/// connection unhealthy and reconnects the WS
|
||||
final int reconnectionMonitorInterval;
|
||||
|
||||
/// Interval of the health event sending timer
|
||||
/// This sends a health event every [healthCheckInterval] seconds in order to
|
||||
/// make the server aware that the client is still listening
|
||||
final int healthCheckInterval;
|
||||
|
||||
/// The timeout that uses the reconnection monitor timer to consider the
|
||||
/// connection unhealthy
|
||||
final int reconnectionMonitorTimeout;
|
||||
|
||||
final BehaviorSubject<ConnectionStatus> _connectionStatusController =
|
||||
BehaviorSubject.seeded(ConnectionStatus.disconnected);
|
||||
|
||||
set _connectionStatus(ConnectionStatus status) =>
|
||||
_connectionStatusController.add(status);
|
||||
|
||||
/// The current connection status value
|
||||
ConnectionStatus? get connectionStatus => _connectionStatusController.value;
|
||||
|
||||
/// This notifies of connection status changes
|
||||
Stream<ConnectionStatus> get connectionStatusStream =>
|
||||
_connectionStatusController.stream;
|
||||
|
||||
late String _path;
|
||||
int _retryAttempt = 1;
|
||||
late WebSocketChannel _channel;
|
||||
Timer? _healthCheck, _reconnectionMonitor;
|
||||
DateTime? _lastEventAt;
|
||||
bool _manuallyDisconnected = false;
|
||||
bool _connecting = false;
|
||||
bool _reconnecting = false;
|
||||
|
||||
Event _decodeEvent(String source) => Event.fromJson(json.decode(source));
|
||||
|
||||
Completer<Event?> _connectionCompleter = Completer<Event?>();
|
||||
|
||||
/// Connect the WS using the parameters passed in the constructor
|
||||
Future<Event?> connect() async {
|
||||
_manuallyDisconnected = false;
|
||||
|
||||
if (_connecting) {
|
||||
logger?.severe('already connecting');
|
||||
return null;
|
||||
}
|
||||
|
||||
_connecting = true;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
|
||||
logger?.info('connecting to $_path');
|
||||
|
||||
_channel =
|
||||
connectFunc?.call(_path) ?? WebSocketChannel.connect(Uri.parse(_path));
|
||||
_channel.stream.listen(
|
||||
(data) async {
|
||||
final jsonData = json.decode(data);
|
||||
if (jsonData['error'] != null) {
|
||||
return _onConnectionError(jsonData['error']);
|
||||
}
|
||||
_onData(data);
|
||||
},
|
||||
onError: (error, stacktrace) {
|
||||
_onConnectionError(error, stacktrace);
|
||||
},
|
||||
onDone: _onDone,
|
||||
);
|
||||
return _connectionCompleter.future;
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
_connecting = false;
|
||||
if (_manuallyDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger?.info('connection closed | closeCode: ${_channel.closeCode} | '
|
||||
'closedReason: ${_channel.closeReason}');
|
||||
|
||||
if (!_reconnecting) {
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _onData(data) {
|
||||
if (_manuallyDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final event = _decodeEvent(data);
|
||||
logger?.info('received new event: $data');
|
||||
|
||||
if (_lastEventAt == null) {
|
||||
logger?.info('connection estabilished');
|
||||
_connecting = false;
|
||||
_reconnecting = false;
|
||||
_lastEventAt = DateTime.now();
|
||||
|
||||
_connectionStatus = ConnectionStatus.connected;
|
||||
_retryAttempt = 1;
|
||||
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
_connectionCompleter.complete(event);
|
||||
}
|
||||
|
||||
_startReconnectionMonitor();
|
||||
_startHealthCheck();
|
||||
}
|
||||
|
||||
handler(event);
|
||||
_lastEventAt = DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> _onConnectionError(error, [stacktrace]) async {
|
||||
logger?..severe('error connecting')..severe(error);
|
||||
if (stacktrace != null) {
|
||||
logger?.severe(stacktrace);
|
||||
}
|
||||
_connecting = false;
|
||||
|
||||
if (!_reconnecting) {
|
||||
_connectionStatus = ConnectionStatus.disconnected;
|
||||
}
|
||||
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
_cancelTimers();
|
||||
_connectionCompleter.completeError(error, stacktrace);
|
||||
} else if (!_reconnecting) {
|
||||
return _reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _reconnectionTimer(_) {
|
||||
final now = DateTime.now();
|
||||
if (_lastEventAt != null &&
|
||||
now.difference(_lastEventAt!).inSeconds > reconnectionMonitorTimeout) {
|
||||
_channel.sink.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _startReconnectionMonitor() {
|
||||
_reconnectionMonitor = Timer.periodic(
|
||||
Duration(seconds: reconnectionMonitorInterval),
|
||||
_reconnectionTimer,
|
||||
);
|
||||
|
||||
_reconnectionTimer(_reconnectionMonitor);
|
||||
}
|
||||
|
||||
void _reconnectTimer() async {
|
||||
if (!_reconnecting) {
|
||||
return;
|
||||
}
|
||||
if (_connecting) {
|
||||
logger?.info('already connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
logger?.info('reconnecting..');
|
||||
|
||||
_cancelTimers();
|
||||
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
logger?.log(Level.SEVERE, e.toString());
|
||||
}
|
||||
await Future.delayed(
|
||||
Duration(seconds: min(_retryAttempt * 5, 25)),
|
||||
() {
|
||||
_reconnectTimer();
|
||||
_retryAttempt++;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reconnect() async {
|
||||
logger?.info('reconnect');
|
||||
if (!_reconnecting) {
|
||||
_reconnecting = true;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
}
|
||||
|
||||
_reconnectTimer();
|
||||
}
|
||||
|
||||
void _cancelTimers() {
|
||||
_lastEventAt = null;
|
||||
if (_healthCheck != null) {
|
||||
_healthCheck!.cancel();
|
||||
}
|
||||
if (_reconnectionMonitor != null) {
|
||||
_reconnectionMonitor!.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void _healthCheckTimer(_) {
|
||||
logger?.info('sending health.check');
|
||||
_channel.sink.add("{'type': 'health.check'}");
|
||||
}
|
||||
|
||||
void _startHealthCheck() {
|
||||
logger?.info('start health check monitor');
|
||||
|
||||
_healthCheck = Timer.periodic(
|
||||
Duration(seconds: healthCheckInterval),
|
||||
_healthCheckTimer,
|
||||
);
|
||||
|
||||
_healthCheckTimer(_healthCheck);
|
||||
}
|
||||
|
||||
/// Disconnects the WS and releases eventual resources
|
||||
Future<void> disconnect() async {
|
||||
_connecting = false;
|
||||
if (!_connectionCompleter.isCompleted) {
|
||||
_connectionCompleter.complete();
|
||||
}
|
||||
if (_manuallyDisconnected) {
|
||||
return;
|
||||
}
|
||||
logger?.info('disconnecting');
|
||||
_connectionCompleter = Completer();
|
||||
_cancelTimers();
|
||||
_reconnecting = false;
|
||||
_manuallyDisconnected = true;
|
||||
_connectionStatus = ConnectionStatus.disconnected;
|
||||
await _connectionStatusController.close();
|
||||
await _channel.sink.close();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+238
-306
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
import 'package:stream_chat/src/client/client.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
|
||||
/// The retry options
|
||||
/// When sending/updating/deleting a message any temporary error will trigger the retry policy
|
||||
/// The retry policy exposes 2 methods
|
||||
/// - shouldRetry: returns a boolean if the request should be retried
|
||||
/// - retryTimeout: How many milliseconds to wait till the next attempt
|
||||
///
|
||||
/// maxRetryAttempts is a hard limit on maximum retry attempts before giving up
|
||||
class RetryPolicy {
|
||||
/// Instantiate a new RetryPolicy
|
||||
RetryPolicy({
|
||||
required this.shouldRetry,
|
||||
required this.retryTimeout,
|
||||
this.maxRetryAttempts = 6,
|
||||
});
|
||||
|
||||
/// Hard limit on maximum retry attempts before giving up, defaults to 6
|
||||
/// Resets once connection recovers.
|
||||
final int maxRetryAttempts;
|
||||
|
||||
/// This function evaluates if we should retry the failure
|
||||
final bool Function(
|
||||
StreamChatClient client,
|
||||
int attempt,
|
||||
StreamChatError? error,
|
||||
) shouldRetry;
|
||||
|
||||
/// In the case that we want to retry a failed request the retryTimeout
|
||||
/// method is called to determine the timeout
|
||||
final Duration Function(
|
||||
StreamChatClient client,
|
||||
int attempt,
|
||||
StreamChatError? error,
|
||||
) retryTimeout;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/client/channel.dart';
|
||||
import 'package:stream_chat/src/client/retry_policy.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// The retry queue associated to a channel
|
||||
class RetryQueue {
|
||||
/// Instantiate a new RetryQueue object
|
||||
RetryQueue({
|
||||
required this.channel,
|
||||
this.logger,
|
||||
}) : client = channel.client {
|
||||
_retryPolicy = client.retryPolicy;
|
||||
_listenConnectionRecovered();
|
||||
_listenFailedEvents();
|
||||
}
|
||||
|
||||
/// The channel of this queue
|
||||
final Channel channel;
|
||||
|
||||
/// The client associated with this [channel]
|
||||
final StreamChatClient client;
|
||||
|
||||
/// The logger associated to this queue
|
||||
final Logger? logger;
|
||||
|
||||
late final RetryPolicy _retryPolicy;
|
||||
|
||||
final _compositeSubscription = CompositeSubscription();
|
||||
|
||||
final _messageQueue = HeapPriorityQueue(_byDate);
|
||||
bool _isRetrying = false;
|
||||
|
||||
void _listenConnectionRecovered() {
|
||||
client.on(EventType.connectionRecovered).listen((event) {
|
||||
if (event.online == true) {
|
||||
_startRetrying();
|
||||
}
|
||||
}).addTo(_compositeSubscription);
|
||||
}
|
||||
|
||||
void _listenFailedEvents() {
|
||||
channel.on().where((event) => event.message != null).listen((event) {
|
||||
final message = event.message!;
|
||||
final containsMessage = _messageQueue.containsMessage(message);
|
||||
if (!containsMessage) return;
|
||||
if (message.status == MessageSendingStatus.sent) {
|
||||
logger?.info('Removing sent message from queue : ${message.id}');
|
||||
_messageQueue.removeMessage(message);
|
||||
return;
|
||||
} else {
|
||||
if ([
|
||||
MessageSendingStatus.failed_update,
|
||||
MessageSendingStatus.failed,
|
||||
MessageSendingStatus.failed_delete,
|
||||
].contains(message.status)) {
|
||||
logger?.info('Adding failed message from event : ${event.type}');
|
||||
add([message]);
|
||||
}
|
||||
}
|
||||
}).addTo(_compositeSubscription);
|
||||
}
|
||||
|
||||
/// Add a list of messages
|
||||
void add(List<Message> messages) {
|
||||
if (messages.isEmpty) return;
|
||||
if (_messageQueue.containsAllMessage(messages)) return;
|
||||
|
||||
logger?.info('Adding ${messages.length} messages');
|
||||
final messageList = _messageQueue.toList();
|
||||
// we should not add message if already available in the queue
|
||||
_messageQueue.addAll(messages.where(
|
||||
(it) => !messageList.any((m) => m.id == it.id),
|
||||
));
|
||||
_startRetrying();
|
||||
}
|
||||
|
||||
Future<void> _startRetrying() async {
|
||||
if (_isRetrying) return;
|
||||
_isRetrying = true;
|
||||
|
||||
logger?.info('Started retrying failed messages');
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
logger?.info('${_messageQueue.length} messages remaining in the queue');
|
||||
final message = _messageQueue.first;
|
||||
await _runAndRetry(message);
|
||||
}
|
||||
_isRetrying = false;
|
||||
}
|
||||
|
||||
Future<void> _runAndRetry(Message message) async {
|
||||
var attempt = 1;
|
||||
|
||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||
// early return in case maxAttempt is less than 0
|
||||
if (attempt > maxAttempt) return;
|
||||
|
||||
// ignore: literal_only_boolean_expressions
|
||||
while (true) {
|
||||
try {
|
||||
logger?.info('Message (${message.id}) retry attempt $attempt');
|
||||
await _retryMessage(message);
|
||||
logger?.info('Message (${message.id}) sent successfully');
|
||||
_messageQueue.removeMessage(message);
|
||||
break;
|
||||
} on StreamChatError catch (e) {
|
||||
// retry logic
|
||||
final maxAttempt = _retryPolicy.maxRetryAttempts;
|
||||
if (attempt < maxAttempt) {
|
||||
final shouldRetry = _retryPolicy.shouldRetry(client, attempt, e);
|
||||
if (shouldRetry) {
|
||||
final timeout = _retryPolicy.retryTimeout(client, attempt, e);
|
||||
// temporary failure, continue
|
||||
logger?.info(
|
||||
'API call failed (attempt $attempt), '
|
||||
'retrying in ${timeout.inSeconds} seconds. Error was $e',
|
||||
);
|
||||
await Future.delayed(timeout);
|
||||
attempt += 1;
|
||||
} else {
|
||||
logger?.info(
|
||||
'API call failed (attempt $attempt). '
|
||||
'Giving up for now, will retry when connection recovers. '
|
||||
'Error was $e',
|
||||
);
|
||||
_sendFailedEvent(message);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logger?.info(
|
||||
'API call failed (attempt $attempt). '
|
||||
'Exceeds maxRetryAttempt : $maxAttempt '
|
||||
'Giving up for now, will retry when connection recovers. '
|
||||
'Error was $e',
|
||||
);
|
||||
_sendFailedEvent(message);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.info(
|
||||
'API call failed due to unknown error (attempt $attempt). '
|
||||
'Giving up for now, will retry when connection recovers. '
|
||||
'Error was $e',
|
||||
);
|
||||
_sendFailedEvent(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _sendFailedEvent(Message message) {
|
||||
final newStatus = message.status == MessageSendingStatus.sending
|
||||
? MessageSendingStatus.failed
|
||||
: message.status == MessageSendingStatus.updating
|
||||
? MessageSendingStatus.failed_update
|
||||
: MessageSendingStatus.failed_delete;
|
||||
channel.state?.addMessage(message.copyWith(status: newStatus));
|
||||
}
|
||||
|
||||
Future<void> _retryMessage(Message message) async {
|
||||
if (message.status == MessageSendingStatus.failed_update ||
|
||||
message.status == MessageSendingStatus.updating) {
|
||||
await channel.updateMessage(message);
|
||||
} else if (message.status == MessageSendingStatus.failed ||
|
||||
message.status == MessageSendingStatus.sending) {
|
||||
await channel.sendMessage(message);
|
||||
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||
message.status == MessageSendingStatus.deleting) {
|
||||
await channel.deleteMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether our [_messageQueue] has messages or not
|
||||
bool get hasMessages => _messageQueue.isNotEmpty;
|
||||
|
||||
/// Call this method to dispose this object
|
||||
void dispose() {
|
||||
_messageQueue.clear();
|
||||
_compositeSubscription.dispose();
|
||||
}
|
||||
|
||||
static int _byDate(Message m1, Message m2) {
|
||||
final date1 = _getMessageDate(m1);
|
||||
final date2 = _getMessageDate(m2);
|
||||
|
||||
if (date1 == null || date2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return date1.compareTo(date2);
|
||||
}
|
||||
|
||||
static DateTime? _getMessageDate(Message m1) {
|
||||
switch (m1.status) {
|
||||
case MessageSendingStatus.failed_delete:
|
||||
case MessageSendingStatus.deleting:
|
||||
return m1.deletedAt;
|
||||
|
||||
case MessageSendingStatus.failed:
|
||||
case MessageSendingStatus.sending:
|
||||
return m1.createdAt;
|
||||
|
||||
case MessageSendingStatus.failed_update:
|
||||
case MessageSendingStatus.updating:
|
||||
return m1.updatedAt;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _MessageHeapPriorityQueue on HeapPriorityQueue<Message> {
|
||||
void removeMessage(Message message) {
|
||||
final list = toUnorderedList();
|
||||
final index = list.indexWhere((it) => it.id == message.id);
|
||||
if (index == -1) return;
|
||||
final element = list[index];
|
||||
remove(element);
|
||||
}
|
||||
|
||||
bool containsMessage(Message message) {
|
||||
final list = toUnorderedList();
|
||||
final index = list.indexWhere((it) => it.id == message.id);
|
||||
if (index == -1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool containsAllMessage(List<Message> messages) {
|
||||
if (isEmpty) return false;
|
||||
final list = toUnorderedList();
|
||||
final messageIds = messages.map((it) => it.id);
|
||||
return list.every((it) => messageIds.contains(it.id));
|
||||
}
|
||||
}
|
||||
+14
-53
@@ -1,8 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/api/responses.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/extensions/string_extension.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||
|
||||
/// Class responsible for uploading images and files to a given channel
|
||||
abstract class AttachmentFileUploader {
|
||||
@@ -60,7 +59,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
/// Creates a new [StreamAttachmentFileUploader] instance.
|
||||
const StreamAttachmentFileUploader(this._client);
|
||||
|
||||
final StreamChatClient _client;
|
||||
final StreamHttpClient _client;
|
||||
|
||||
@override
|
||||
Future<SendImageResponse> sendImage(
|
||||
@@ -70,33 +69,14 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
ProgressCallback? onSendProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/').last ?? file.name;
|
||||
final mimeType = filename?.mimeType;
|
||||
|
||||
MultipartFile? multiPartFile;
|
||||
if (file.path != null) {
|
||||
multiPartFile = await MultipartFile.fromFile(
|
||||
file.path!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
} else if (file.bytes != null) {
|
||||
multiPartFile = MultipartFile.fromBytes(
|
||||
file.bytes!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
final multiPartFile = await file.toMultipartFile();
|
||||
final response = await _client.postFile(
|
||||
'/channels/$channelType/$channelId/image',
|
||||
data: FormData.fromMap({
|
||||
'file': multiPartFile,
|
||||
}),
|
||||
multiPartFile,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, SendImageResponse.fromJson);
|
||||
return SendImageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -107,33 +87,14 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
ProgressCallback? onSendProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/').last ?? file.name;
|
||||
final mimeType = filename?.mimeType;
|
||||
|
||||
MultipartFile? multiPartFile;
|
||||
if (file.path != null) {
|
||||
multiPartFile = await MultipartFile.fromFile(
|
||||
file.path!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
} else if (file.bytes != null) {
|
||||
multiPartFile = MultipartFile.fromBytes(
|
||||
file.bytes!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
final multiPartFile = await file.toMultipartFile();
|
||||
final response = await _client.postFile(
|
||||
'/channels/$channelType/$channelId/file',
|
||||
data: FormData.fromMap({
|
||||
'file': multiPartFile,
|
||||
}),
|
||||
multiPartFile,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, SendFileResponse.fromJson);
|
||||
return SendFileResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -148,7 +109,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
queryParameters: {'url': url},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, EmptyResponse.fromJson);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -163,6 +124,6 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
queryParameters: {'url': url},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, EmptyResponse.fromJson);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/core/models/event.dart';
|
||||
import 'package:stream_chat/src/core/models/filter.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
|
||||
/// Defines the api dedicated to channel operations
|
||||
class ChannelApi {
|
||||
/// Initialize a new channel api
|
||||
ChannelApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
String _getChannelUrl(String channelId, String channelType) =>
|
||||
'/channels/$channelType/$channelId';
|
||||
|
||||
/// Query the API, get messages, members or other channel fields
|
||||
Future<ChannelState> queryChannel(
|
||||
String channelType, {
|
||||
bool state = true,
|
||||
bool watch = false,
|
||||
bool presence = false,
|
||||
String? channelId,
|
||||
Map<String, Object?>? channelData,
|
||||
PaginationParams? messagesPagination,
|
||||
PaginationParams? membersPagination,
|
||||
PaginationParams? watchersPagination,
|
||||
}) async {
|
||||
var channelPath = '/channels/$channelType';
|
||||
if (channelId != null) channelPath = '$channelPath/$channelId';
|
||||
final response = await _client.post(
|
||||
'$channelPath/query',
|
||||
data: {
|
||||
'state': state,
|
||||
'watch': watch,
|
||||
'presence': presence,
|
||||
if (channelData != null) 'data': channelData,
|
||||
if (messagesPagination != null) 'messages': messagesPagination,
|
||||
if (membersPagination != null) 'members': membersPagination,
|
||||
if (watchersPagination != null) 'watchers': watchersPagination,
|
||||
},
|
||||
);
|
||||
return ChannelState.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Requests channels with a given query from the API.
|
||||
Future<QueryChannelsResponse> queryChannels({
|
||||
Filter? filter,
|
||||
List<SortOption<ChannelModel>>? sort,
|
||||
int? memberLimit,
|
||||
int? messageLimit,
|
||||
bool state = true,
|
||||
bool watch = true,
|
||||
bool presence = false,
|
||||
PaginationParams paginationParams = const PaginationParams(),
|
||||
}) async {
|
||||
final response = await _client.get(
|
||||
'/channels',
|
||||
queryParameters: {
|
||||
'payload': jsonEncode({
|
||||
// default options
|
||||
'state': state,
|
||||
'watch': watch,
|
||||
'presence': presence,
|
||||
|
||||
// passed options
|
||||
if (sort != null) 'sort': sort,
|
||||
if (filter != null) 'filter_conditions': filter,
|
||||
if (memberLimit != null) 'member_limit': memberLimit,
|
||||
if (messageLimit != null) 'message_limit': messageLimit,
|
||||
|
||||
// pagination
|
||||
...paginationParams.toJson()
|
||||
}),
|
||||
},
|
||||
);
|
||||
return QueryChannelsResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Mark all channels for this user as read
|
||||
Future<EmptyResponse> markAllRead() async {
|
||||
final response = await _client.post('channels/read');
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Replaces the [channelId] of type [ChannelType] data with [data]
|
||||
Future<UpdateChannelResponse> updateChannel(
|
||||
String channelId,
|
||||
String channelType,
|
||||
Map<String, Object?> data, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'data': data,
|
||||
if (message != null)
|
||||
'message': message.copyWith(updatedAt: DateTime.now()),
|
||||
},
|
||||
);
|
||||
return UpdateChannelResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Updates the [channelId] of type [ChannelType] data with [data]
|
||||
Future<PartialUpdateChannelResponse> updateChannelPartial(
|
||||
String channelId,
|
||||
String channelType, {
|
||||
Map<String, Object?>? set,
|
||||
List<String>? unset,
|
||||
}) async {
|
||||
final response = await _client.patch(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
if (set != null) 'set': set,
|
||||
if (unset != null) 'unset': unset,
|
||||
},
|
||||
);
|
||||
return PartialUpdateChannelResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Accept invitation to the channel
|
||||
Future<AcceptInviteResponse> acceptChannelInvite(
|
||||
String channelId,
|
||||
String channelType, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'accept_invite': true,
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return AcceptInviteResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Reject invitation to the channel
|
||||
Future<RejectInviteResponse> rejectChannelInvite(
|
||||
String channelId,
|
||||
String channelType, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'reject_invite': true,
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return RejectInviteResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Invite members to the channel
|
||||
Future<InviteMembersResponse> inviteChannelMembers(
|
||||
String channelId,
|
||||
String channelType,
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'invites': memberIds,
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return InviteMembersResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Add members to the channel
|
||||
Future<AddMembersResponse> addMembers(
|
||||
String channelId,
|
||||
String channelType,
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'add_members': memberIds,
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return AddMembersResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Remove members from the channel
|
||||
Future<RemoveMembersResponse> removeMembers(
|
||||
String channelId,
|
||||
String channelType,
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
data: {
|
||||
'remove_members': memberIds,
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
return RemoveMembersResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Send an event on this channel
|
||||
Future<EmptyResponse> sendEvent(
|
||||
String channelId,
|
||||
String channelType,
|
||||
Event event,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/event',
|
||||
data: {'event': event},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Delete this channel. Messages are permanently removed.
|
||||
Future<EmptyResponse> deleteChannel(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
final response = await _client.delete(
|
||||
_getChannelUrl(channelId, channelType),
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Removes all messages from the channel
|
||||
Future<EmptyResponse> truncateChannel(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/truncate',
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Hides the channel from [StreamChatClient.queryChannels] for the user
|
||||
/// until a message is added If [clearHistory] is set to true - all messages
|
||||
/// will be removed for the user
|
||||
Future<EmptyResponse> hideChannel(
|
||||
String channelId,
|
||||
String channelType, {
|
||||
bool clearHistory = false,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/hide',
|
||||
data: {'clear_history': clearHistory},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Removes the hidden status for the channel
|
||||
Future<EmptyResponse> showChannel(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/show',
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Mark [channelId] of type [channelType] all messages as read
|
||||
/// Optionally provide a [messageId] if you want to mark a
|
||||
/// particular message as read
|
||||
Future<EmptyResponse> markRead(
|
||||
String channelId,
|
||||
String channelType, {
|
||||
String? messageId,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/read',
|
||||
data: {if (messageId != null) 'message_id': messageId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Stop watching the channel
|
||||
Future<EmptyResponse> stopWatching(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/stop-watching',
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
|
||||
/// Provider used to send push notifications.
|
||||
enum PushProvider {
|
||||
/// Send notifications using Google's Firebase Cloud Messaging
|
||||
firebase,
|
||||
|
||||
/// Send notifications using Apple's Push Notification service
|
||||
apn
|
||||
}
|
||||
|
||||
/// Helper extension for [PushProvider]
|
||||
extension PushProviderX on PushProvider {
|
||||
/// Returns the string notion for [PushProvider].
|
||||
String get name => {
|
||||
PushProvider.apn: 'apn',
|
||||
PushProvider.firebase: 'firebase',
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
/// Defines the api dedicated to device operations
|
||||
class DeviceApi {
|
||||
/// Initialize a new device api
|
||||
DeviceApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Add a device for Push Notifications.
|
||||
Future<EmptyResponse> addDevice(
|
||||
String deviceId,
|
||||
PushProvider pushProvider,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/devices',
|
||||
data: {
|
||||
'id': deviceId,
|
||||
'push_provider': pushProvider.name,
|
||||
},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Gets a list of user devices.
|
||||
Future<ListDevicesResponse> getDevices() async {
|
||||
final response = await _client.get('/devices');
|
||||
return ListDevicesResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Remove a user's device.
|
||||
Future<EmptyResponse> removeDevice(
|
||||
String deviceId,
|
||||
) async {
|
||||
final response = await _client.delete(
|
||||
'/devices',
|
||||
queryParameters: {'id': deviceId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/filter.dart';
|
||||
import 'package:stream_chat/src/core/models/member.dart';
|
||||
|
||||
/// Defines the api dedicated to general operations
|
||||
class GeneralApi {
|
||||
/// Initialize a new general api
|
||||
GeneralApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Get all the missed events
|
||||
Future<SyncResponse> sync(
|
||||
List<String> cids,
|
||||
DateTime lastSyncAt,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/sync',
|
||||
data: {
|
||||
'channel_cids': cids,
|
||||
'last_sync_at': lastSyncAt.toUtc().toIso8601String(),
|
||||
},
|
||||
);
|
||||
return SyncResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// A message search.
|
||||
Future<SearchMessagesResponse> searchMessages(
|
||||
Filter filter, {
|
||||
String? query,
|
||||
List<SortOption>? sort,
|
||||
PaginationParams? pagination,
|
||||
Filter? messageFilters,
|
||||
}) async {
|
||||
assert(() {
|
||||
if (query == null && messageFilters == null) {
|
||||
throw ArgumentError('Provide at least `query` or `messageFilters`');
|
||||
}
|
||||
if (query != null && messageFilters != null) {
|
||||
throw ArgumentError(
|
||||
"Can't provide both `query` and `messageFilters` at the same time",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}(), 'Check incoming params.');
|
||||
|
||||
final response = await _client.get(
|
||||
'/search',
|
||||
queryParameters: {
|
||||
'payload': jsonEncode({
|
||||
'filter_conditions': filter,
|
||||
if (sort != null) 'sort': sort,
|
||||
if (query != null) 'query': query,
|
||||
if (messageFilters != null)
|
||||
'message_filter_conditions': messageFilters,
|
||||
if (pagination != null) ...pagination.toJson(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return SearchMessagesResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Query channel members
|
||||
Future<QueryMembersResponse> queryMembers(
|
||||
String channelType, {
|
||||
Filter? filter,
|
||||
String? channelId,
|
||||
List<Member>? members,
|
||||
List<SortOption>? sort,
|
||||
PaginationParams? pagination,
|
||||
}) async {
|
||||
final response = await _client.get(
|
||||
'/members',
|
||||
queryParameters: {
|
||||
'payload': jsonEncode({
|
||||
'type': channelType,
|
||||
'filter_conditions': filter ?? {},
|
||||
if (channelId != null)
|
||||
'id': channelId
|
||||
else if (members != null)
|
||||
'members': members,
|
||||
if (sort != null) 'sort': sort,
|
||||
if (pagination != null) ...pagination.toJson(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return QueryMembersResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
/// Defines the api dedicated to guest users operations
|
||||
class GuestApi {
|
||||
/// Initialize a new guest api
|
||||
GuestApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Returns the information about guest user
|
||||
Future<ConnectGuestUserResponse> getGuestUser(User user) async {
|
||||
final response = await _client.post(
|
||||
'/guest',
|
||||
data: {'user': user},
|
||||
);
|
||||
return ConnectGuestUserResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
|
||||
/// Defines the api dedicated to messages operations
|
||||
class MessageApi {
|
||||
/// Initialize a new message api
|
||||
MessageApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Sends the [message] to the given [channelId] of given [channelType]
|
||||
Future<SendMessageResponse> sendMessage(
|
||||
String channelId,
|
||||
String channelType,
|
||||
Message message, {
|
||||
bool skipPush = false,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'/channels/$channelType/$channelId/message',
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_push': skipPush,
|
||||
},
|
||||
);
|
||||
return SendMessageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Retrieves a list of messages by [messageIDs]
|
||||
/// from the given [channelId] of type [channelType]
|
||||
Future<GetMessagesByIdResponse> getMessagesById(
|
||||
String channelId,
|
||||
String channelType,
|
||||
List<String> messageIDs,
|
||||
) async {
|
||||
final response = await _client.get(
|
||||
'/channels/$channelType/$channelId/messages',
|
||||
queryParameters: {'ids': messageIDs.join(',')},
|
||||
);
|
||||
return GetMessagesByIdResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Get a message by [messageId]
|
||||
Future<GetMessageResponse> getMessage(String messageId) async {
|
||||
final response = await _client.get(
|
||||
'/messages/$messageId',
|
||||
);
|
||||
return GetMessageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Updates the given [message]
|
||||
Future<UpdateMessageResponse> updateMessage(
|
||||
Message message,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/messages/${message.id}',
|
||||
data: {'message': message},
|
||||
);
|
||||
return UpdateMessageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Partially update the given [messageId]
|
||||
/// Use [set] to define values to be set
|
||||
/// Use [unset] to define values to be unset
|
||||
Future<UpdateMessageResponse> partialUpdateMessage(
|
||||
String messageId, {
|
||||
Map<String, Object?>? set,
|
||||
List<String>? unset,
|
||||
}) async {
|
||||
final response = await _client.put(
|
||||
'/messages/$messageId',
|
||||
data: {
|
||||
if (set != null) 'set': set,
|
||||
if (unset != null) 'unset': unset,
|
||||
},
|
||||
);
|
||||
return UpdateMessageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Deletes the given [messageId]
|
||||
Future<EmptyResponse> deleteMessage(
|
||||
String messageId,
|
||||
) async {
|
||||
final response = await _client.delete(
|
||||
'/messages/$messageId',
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Send action for a specific [messageId]
|
||||
/// of the given [channelId] of given [channelType]
|
||||
Future<SendActionResponse> sendAction(
|
||||
String channelId,
|
||||
String channelType,
|
||||
String messageId,
|
||||
Map<String, Object?> formData,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/messages/$messageId/action',
|
||||
data: {
|
||||
'id': channelId,
|
||||
'type': channelType,
|
||||
'form_data': formData,
|
||||
'message_id': messageId,
|
||||
},
|
||||
);
|
||||
return SendActionResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Send a [reactionType] for this [messageId]
|
||||
/// Set [enforceUnique] to true to remove the existing user reaction
|
||||
Future<SendReactionResponse> sendReaction(
|
||||
String messageId,
|
||||
String reactionType, {
|
||||
Map<String, Object?> extraData = const {},
|
||||
bool enforceUnique = false,
|
||||
}) async {
|
||||
final reaction = Map<String, Object?>.from(extraData)
|
||||
..addAll({'type': reactionType});
|
||||
|
||||
final response = await _client.post(
|
||||
'/messages/$messageId/reaction',
|
||||
data: {
|
||||
'reaction': reaction,
|
||||
'enforce_unique': enforceUnique,
|
||||
},
|
||||
);
|
||||
return SendReactionResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Delete a [reactionType] from this [messageId]
|
||||
Future<EmptyResponse> deleteReaction(
|
||||
String messageId,
|
||||
String reactionType,
|
||||
) async {
|
||||
final response = await _client.delete(
|
||||
'/messages/$messageId/reaction/$reactionType',
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Get all the reactions for a [messageId]
|
||||
Future<QueryReactionsResponse> getReactions(
|
||||
String messageId, {
|
||||
PaginationParams? pagination,
|
||||
}) async {
|
||||
final response = await _client.get(
|
||||
'/messages/$messageId/reactions',
|
||||
queryParameters: {
|
||||
if (pagination != null) ...pagination.toJson(),
|
||||
},
|
||||
);
|
||||
return QueryReactionsResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Translates the [messageId] in provided [language]
|
||||
Future<TranslateMessageResponse> translateMessage(
|
||||
String messageId,
|
||||
String language,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/messages/$messageId/translate',
|
||||
data: {'language': language},
|
||||
);
|
||||
return TranslateMessageResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Lists all the message replies for the [parentId]
|
||||
Future<QueryRepliesResponse> getReplies(
|
||||
String parentId, {
|
||||
PaginationParams? options,
|
||||
}) async {
|
||||
final response = await _client.get(
|
||||
'/messages/$parentId/replies',
|
||||
queryParameters: {
|
||||
if (options != null) ...options.toJson(),
|
||||
},
|
||||
);
|
||||
return QueryRepliesResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
|
||||
/// Defines the api dedicated to moderation operations
|
||||
class ModerationApi {
|
||||
/// Initialize a new moderation api
|
||||
ModerationApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Mutes a user
|
||||
Future<EmptyResponse> muteUser(String userId) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/mute',
|
||||
data: {'target_id': userId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Unmutes a user
|
||||
Future<EmptyResponse> unmuteUser(String userId) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/unmute',
|
||||
data: {'target_id': userId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Mutes the channel
|
||||
Future<EmptyResponse> muteChannel(
|
||||
String channelCid, {
|
||||
Duration? expiration,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/mute/channel',
|
||||
data: {
|
||||
'channel_cid': channelCid,
|
||||
if (expiration != null) 'expiration': expiration.inMilliseconds,
|
||||
},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Unmutes the channel
|
||||
Future<EmptyResponse> unmuteChannel(
|
||||
String channelCid,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/unmute/channel',
|
||||
data: {'channel_cid': channelCid},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Flag a message
|
||||
Future<EmptyResponse> flagMessage(
|
||||
String messageId,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/flag',
|
||||
data: {'target_message_id': messageId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Unflag a message
|
||||
Future<EmptyResponse> unflagMessage(
|
||||
String messageId,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/unflag',
|
||||
data: {'target_message_id': messageId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Flag a user
|
||||
Future<EmptyResponse> flagUser(
|
||||
String userId,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/flag',
|
||||
data: {'target_user_id': userId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Unflag a user
|
||||
Future<EmptyResponse> unflagUser(
|
||||
String userId,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/unflag',
|
||||
data: {'target_user_id': userId},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Bans a user from all channels
|
||||
Future<EmptyResponse> banUser(
|
||||
String targetUserId, {
|
||||
Map<String, Object?>? options,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'/moderation/ban',
|
||||
data: {
|
||||
'target_user_id': targetUserId,
|
||||
if (options != null) ...options,
|
||||
},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Remove global ban for a user
|
||||
Future<EmptyResponse> unbanUser(
|
||||
String targetUserId, {
|
||||
Map<String, Object?>? options,
|
||||
}) async {
|
||||
final response = await _client.delete(
|
||||
'/moderation/ban',
|
||||
queryParameters: {
|
||||
'target_user_id': targetUserId,
|
||||
if (options != null) ...options,
|
||||
},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
+20
-23
@@ -1,9 +1,10 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'requests.g.dart';
|
||||
|
||||
/// Sorting options
|
||||
@JsonSerializable(createFactory: false)
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class SortOption<T> {
|
||||
/// Creates a new SortOption instance
|
||||
///
|
||||
@@ -18,6 +19,10 @@ class SortOption<T> {
|
||||
this.comparator,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory SortOption.fromJson(Map<String, dynamic> json) =>
|
||||
_$SortOptionFromJson(json);
|
||||
|
||||
/// Ascending order
|
||||
// ignore: constant_identifier_names
|
||||
static const ASC = 1;
|
||||
@@ -41,8 +46,8 @@ class SortOption<T> {
|
||||
}
|
||||
|
||||
/// Pagination options.
|
||||
@JsonSerializable(createFactory: false, includeIfNull: false)
|
||||
class PaginationParams {
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class PaginationParams extends Equatable {
|
||||
/// Creates a new PaginationParams instance
|
||||
///
|
||||
/// For example:
|
||||
@@ -62,6 +67,10 @@ class PaginationParams {
|
||||
this.lessThanOrEqual,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory PaginationParams.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaginationParamsFromJson(json);
|
||||
|
||||
/// The amount of items requested from the APIs.
|
||||
final int limit;
|
||||
|
||||
@@ -106,24 +115,12 @@ class PaginationParams {
|
||||
);
|
||||
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
int get hashCode =>
|
||||
runtimeType.hashCode ^
|
||||
limit.hashCode ^
|
||||
offset.hashCode ^
|
||||
greaterThan.hashCode ^
|
||||
greaterThanOrEqual.hashCode ^
|
||||
lessThan.hashCode ^
|
||||
lessThanOrEqual.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant PaginationParams other) =>
|
||||
identical(this, other) ||
|
||||
runtimeType == other.runtimeType &&
|
||||
limit == other.limit &&
|
||||
offset == other.offset &&
|
||||
greaterThan == other.greaterThan &&
|
||||
greaterThanOrEqual == other.greaterThanOrEqual &&
|
||||
lessThan == other.lessThan &&
|
||||
lessThanOrEqual == other.lessThanOrEqual;
|
||||
List<Object?> get props => [
|
||||
limit,
|
||||
offset,
|
||||
greaterThan,
|
||||
greaterThanOrEqual,
|
||||
lessThan,
|
||||
lessThanOrEqual,
|
||||
];
|
||||
}
|
||||
+18
@@ -6,12 +6,30 @@ part of 'requests.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SortOption<T> _$SortOptionFromJson<T>(Map<String, dynamic> json) {
|
||||
return SortOption<T>(
|
||||
json['field'] as String,
|
||||
direction: json['direction'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
||||
<String, dynamic>{
|
||||
'field': instance.field,
|
||||
'direction': instance.direction,
|
||||
};
|
||||
|
||||
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) {
|
||||
return PaginationParams(
|
||||
limit: json['limit'] as int,
|
||||
offset: json['offset'] as int,
|
||||
greaterThan: json['id_gt'] as String?,
|
||||
greaterThanOrEqual: json['id_gte'] as String?,
|
||||
lessThan: json['id_lt'] as String?,
|
||||
lessThanOrEqual: json['id_lte'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||
final val = <String, dynamic>{
|
||||
'limit': instance.limit,
|
||||
+42
-11
@@ -1,14 +1,15 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/models/device.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
import 'package:stream_chat/src/models/member.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/reaction.dart';
|
||||
import 'package:stream_chat/src/models/read.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/client/client.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/core/models/device.dart';
|
||||
import 'package:stream_chat/src/core/models/event.dart';
|
||||
import 'package:stream_chat/src/core/models/member.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/models/reaction.dart';
|
||||
import 'package:stream_chat/src/core/models/read.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'responses.g.dart';
|
||||
|
||||
@@ -16,7 +17,37 @@ class _BaseResponse {
|
||||
String? duration;
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.resync] api call
|
||||
/// Model response for [StreamChatNetworkError] data
|
||||
@JsonSerializable()
|
||||
class ErrorResponse extends _BaseResponse {
|
||||
/// The http error code
|
||||
int? code;
|
||||
|
||||
/// The message associated to the error code
|
||||
String? message;
|
||||
|
||||
/// The backend error code
|
||||
@JsonKey(name: 'StatusCode')
|
||||
int? statusCode;
|
||||
|
||||
/// A detailed message about the error
|
||||
String? moreInfo;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static ErrorResponse fromJson(Map<String, dynamic> json) =>
|
||||
_$ErrorResponseFromJson(json);
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$ErrorResponseToJson(this);
|
||||
|
||||
@override
|
||||
String toString() => 'ErrorResponse(code: $code, '
|
||||
'message: $message, '
|
||||
'statusCode: $statusCode, '
|
||||
'moreInfo: $moreInfo)';
|
||||
}
|
||||
|
||||
/// Model response for [StreamChatClient.sync] api call
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SyncResponse extends _BaseResponse {
|
||||
/// The list of events
|
||||
+18
@@ -6,6 +6,24 @@ part of 'responses.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ErrorResponse _$ErrorResponseFromJson(Map<String, dynamic> json) {
|
||||
return ErrorResponse()
|
||||
..duration = json['duration'] as String?
|
||||
..code = json['code'] as int?
|
||||
..message = json['message'] as String?
|
||||
..statusCode = json['StatusCode'] as int?
|
||||
..moreInfo = json['more_info'] as String?;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'duration': instance.duration,
|
||||
'code': instance.code,
|
||||
'message': instance.message,
|
||||
'StatusCode': instance.statusCode,
|
||||
'more_info': instance.moreInfo,
|
||||
};
|
||||
|
||||
SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) {
|
||||
return SyncResponse()
|
||||
..duration = json['duration'] as String?
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:stream_chat/src/core/api/attachment_file_uploader.dart';
|
||||
import 'package:stream_chat/src/core/api/channel_api.dart';
|
||||
import 'package:stream_chat/src/core/api/device_api.dart';
|
||||
import 'package:stream_chat/src/core/api/general_api.dart';
|
||||
import 'package:stream_chat/src/core/api/guest_api.dart';
|
||||
import 'package:stream_chat/src/core/api/message_api.dart';
|
||||
import 'package:stream_chat/src/core/api/moderation_api.dart';
|
||||
import 'package:stream_chat/src/core/api/user_api.dart';
|
||||
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||
|
||||
export 'device_api.dart' show PushProvider;
|
||||
|
||||
/// ApiClient that wraps every other specific api
|
||||
class StreamChatApi {
|
||||
/// Initialize a new stream chat api
|
||||
StreamChatApi(
|
||||
String apiKey, {
|
||||
StreamHttpClient? client,
|
||||
StreamHttpClientOptions? options,
|
||||
TokenManager? tokenManager,
|
||||
ConnectionIdManager? connectionIdManager,
|
||||
AttachmentFileUploader? attachmentFileUploader,
|
||||
Logger? logger,
|
||||
}) : _fileUploader = attachmentFileUploader,
|
||||
_client = client ??
|
||||
StreamHttpClient(
|
||||
apiKey,
|
||||
options: options,
|
||||
tokenManager: tokenManager,
|
||||
connectionIdManager: connectionIdManager,
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
UserApi? _user;
|
||||
|
||||
/// Api dedicated to users operations
|
||||
UserApi get user => _user ??= UserApi(_client);
|
||||
|
||||
GuestApi? _guest;
|
||||
|
||||
/// Api dedicated to guest operations
|
||||
GuestApi get guest => _guest ??= GuestApi(_client);
|
||||
|
||||
MessageApi? _message;
|
||||
|
||||
/// Api dedicated to message operations
|
||||
MessageApi get message => _message ??= MessageApi(_client);
|
||||
|
||||
ChannelApi? _channel;
|
||||
|
||||
/// Api dedicated to channel operations
|
||||
ChannelApi get channel => _channel ??= ChannelApi(_client);
|
||||
|
||||
DeviceApi? _device;
|
||||
|
||||
/// Api dedicated to device operations
|
||||
DeviceApi get device => _device ??= DeviceApi(_client);
|
||||
|
||||
ModerationApi? _moderation;
|
||||
|
||||
/// Api dedicated to moderation operations
|
||||
ModerationApi get moderation => _moderation ??= ModerationApi(_client);
|
||||
|
||||
GeneralApi? _general;
|
||||
|
||||
/// Api dedicated to general operations
|
||||
GeneralApi get general => _general ??= GeneralApi(_client);
|
||||
|
||||
AttachmentFileUploader? _fileUploader;
|
||||
|
||||
/// Class responsible for uploading images and files to a given channel
|
||||
AttachmentFileUploader get fileUploader =>
|
||||
_fileUploader ??= StreamAttachmentFileUploader(_client);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/models/filter.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
/// Defines the api dedicated to users operations
|
||||
class UserApi {
|
||||
/// Initialize a new user api
|
||||
UserApi(this._client);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// Requests users with a given query.
|
||||
Future<QueryUsersResponse> queryUsers({
|
||||
bool presence = false,
|
||||
Filter? filter,
|
||||
List<SortOption>? sort,
|
||||
PaginationParams? pagination,
|
||||
}) async {
|
||||
final response = await _client.get(
|
||||
'/users',
|
||||
queryParameters: {
|
||||
'payload': jsonEncode({
|
||||
'presence': presence,
|
||||
if (sort != null) 'sort': sort,
|
||||
if (filter != null) 'filter_conditions': filter,
|
||||
if (pagination != null) ...pagination.toJson(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return QueryUsersResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Batch update a list of users
|
||||
Future<UpdateUsersResponse> updateUsers(
|
||||
List<User> users,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/users',
|
||||
data: {
|
||||
'users': {for (final user in users) user.id: user},
|
||||
},
|
||||
);
|
||||
return UpdateUsersResponse.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
/// Complete list of errors that are returned by the API
|
||||
/// together with the description and API code.
|
||||
enum ChatErrorCode {
|
||||
// Client errors
|
||||
|
||||
/// Unauthenticated, token not defined
|
||||
undefinedToken,
|
||||
|
||||
// Bad Request
|
||||
|
||||
/// Wrong data/parameter is sent to the API
|
||||
inputError,
|
||||
|
||||
/// Duplicate username is sent while enforce_unique_usernames is enabled
|
||||
duplicateUsername,
|
||||
|
||||
/// Message is too long
|
||||
messageTooLong,
|
||||
|
||||
/// Event is not supported
|
||||
eventNotSupported,
|
||||
|
||||
/// The feature is currently disabled
|
||||
/// on the dashboard (i.e. Reactions & Replies)
|
||||
channelFeatureNotSupported,
|
||||
|
||||
/// Multiple Levels Reply is not supported
|
||||
/// the API only supports 1 level deep reply threads
|
||||
multipleNestling,
|
||||
|
||||
/// Custom Command handler returned an error
|
||||
customCommandEndpointCall,
|
||||
|
||||
/// App config does not have custom_action_handler_url
|
||||
customCommandEndpointMissing,
|
||||
|
||||
// Unauthorised
|
||||
|
||||
/// Unauthenticated, problem with authentication
|
||||
authenticationError,
|
||||
|
||||
/// Unauthenticated, token expired
|
||||
tokenExpired,
|
||||
|
||||
/// Unauthenticated, token date incorrect
|
||||
tokenBeforeIssuedAt,
|
||||
|
||||
/// Unauthenticated, token not valid yet
|
||||
tokenNotValid,
|
||||
|
||||
/// Unauthenticated, token signature invalid
|
||||
tokenSignatureInvalid,
|
||||
|
||||
/// Access Key invalid
|
||||
accessKeyError,
|
||||
|
||||
// Forbidden
|
||||
|
||||
/// Unauthorised / forbidden to make request
|
||||
notAllowed,
|
||||
|
||||
/// App suspended
|
||||
appSuspended,
|
||||
|
||||
/// User tried to post a message during the cooldown period
|
||||
cooldownError,
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
/// Resource not found
|
||||
doesNotExist,
|
||||
|
||||
/// Request timed out
|
||||
requestTimeout,
|
||||
|
||||
/// Payload too big
|
||||
payloadTooBig,
|
||||
|
||||
/// Too many requests in a certain time frame
|
||||
rateLimitError,
|
||||
|
||||
/// Request headers are too large
|
||||
maximumHeaderSizeExceeded,
|
||||
|
||||
/// Something goes wrong in the system
|
||||
internalSystemError,
|
||||
|
||||
/// No access to requested channels
|
||||
noAccessToChannels
|
||||
}
|
||||
|
||||
const _errorCodeWithDescription = {
|
||||
ChatErrorCode.undefinedToken:
|
||||
MapEntry(1000, 'Unauthorised, token not defined'),
|
||||
ChatErrorCode.inputError:
|
||||
MapEntry(4, 'Wrong data/parameter is sent to the API'),
|
||||
ChatErrorCode.duplicateUsername: MapEntry(6,
|
||||
'Duplicate username is sent while enforce_unique_usernames is enabled'),
|
||||
ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'),
|
||||
ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'),
|
||||
ChatErrorCode.channelFeatureNotSupported: MapEntry(19,
|
||||
'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)'),
|
||||
ChatErrorCode.multipleNestling: MapEntry(21,
|
||||
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads'),
|
||||
ChatErrorCode.customCommandEndpointCall:
|
||||
MapEntry(45, 'Custom Command handler returned an error'),
|
||||
ChatErrorCode.customCommandEndpointMissing:
|
||||
MapEntry(44, 'App config does not have custom_action_handler_url'),
|
||||
ChatErrorCode.authenticationError:
|
||||
MapEntry(5, 'Unauthenticated, problem with authentication'),
|
||||
ChatErrorCode.tokenExpired: MapEntry(40, 'Unauthenticated, token expired'),
|
||||
ChatErrorCode.tokenBeforeIssuedAt:
|
||||
MapEntry(42, 'Unauthenticated, token date incorrect'),
|
||||
ChatErrorCode.tokenNotValid:
|
||||
MapEntry(41, 'Unauthenticated, token not valid yet'),
|
||||
ChatErrorCode.tokenSignatureInvalid:
|
||||
MapEntry(43, 'Unauthenticated, token signature invalid'),
|
||||
ChatErrorCode.accessKeyError: MapEntry(2, 'Access Key invalid'),
|
||||
ChatErrorCode.notAllowed:
|
||||
MapEntry(17, 'Unauthorised / forbidden to make request'),
|
||||
ChatErrorCode.appSuspended: MapEntry(99, 'App suspended'),
|
||||
ChatErrorCode.cooldownError:
|
||||
MapEntry(60, 'User tried to post a message during the cooldown period'),
|
||||
ChatErrorCode.doesNotExist: MapEntry(16, 'Resource not found'),
|
||||
ChatErrorCode.requestTimeout: MapEntry(23, 'Request timed out'),
|
||||
ChatErrorCode.payloadTooBig: MapEntry(22, 'Payload too big'),
|
||||
ChatErrorCode.rateLimitError:
|
||||
MapEntry(9, 'Too many requests in a certain time frame'),
|
||||
ChatErrorCode.maximumHeaderSizeExceeded:
|
||||
MapEntry(24, 'Request headers are too large'),
|
||||
ChatErrorCode.internalSystemError:
|
||||
MapEntry(-1, 'Something goes wrong in the system'),
|
||||
ChatErrorCode.noAccessToChannels:
|
||||
MapEntry(70, 'No access to requested channels'),
|
||||
};
|
||||
|
||||
const _authenticationErrors = [
|
||||
ChatErrorCode.undefinedToken,
|
||||
ChatErrorCode.authenticationError,
|
||||
ChatErrorCode.tokenExpired,
|
||||
ChatErrorCode.tokenBeforeIssuedAt,
|
||||
ChatErrorCode.tokenNotValid,
|
||||
ChatErrorCode.tokenSignatureInvalid,
|
||||
ChatErrorCode.accessKeyError,
|
||||
ChatErrorCode.noAccessToChannels,
|
||||
];
|
||||
|
||||
///
|
||||
ChatErrorCode? chatErrorCodeFromCode(int code) => _errorCodeWithDescription.keys
|
||||
.firstWhereOrNull((key) => _errorCodeWithDescription[key]!.key == code);
|
||||
|
||||
///
|
||||
extension ChatErrorCodeX on ChatErrorCode {
|
||||
///
|
||||
String get message => _errorCodeWithDescription[this]!.value;
|
||||
|
||||
///
|
||||
int get code => _errorCodeWithDescription[this]!.key;
|
||||
|
||||
///
|
||||
bool get isAuthenticationError => _authenticationErrors.contains(this);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'chat_error_code.dart';
|
||||
export 'stream_chat_error.dart';
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:stream_chat/src/core/error/chat_error_code.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
///
|
||||
class StreamChatError with EquatableMixin implements Exception {
|
||||
///
|
||||
const StreamChatError(this.message);
|
||||
|
||||
/// Error message
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
|
||||
@override
|
||||
String toString() => 'StreamChatError(message: $message)';
|
||||
}
|
||||
|
||||
///
|
||||
class StreamWebSocketError extends StreamChatError {
|
||||
///
|
||||
const StreamWebSocketError(
|
||||
String message, {
|
||||
this.data,
|
||||
}) : super(message);
|
||||
|
||||
///
|
||||
factory StreamWebSocketError.fromStreamError(Map<String, Object?> error) {
|
||||
final data = ErrorResponse.fromJson(error);
|
||||
final message = data.message ?? '';
|
||||
return StreamWebSocketError(message, data: data);
|
||||
}
|
||||
|
||||
///
|
||||
factory StreamWebSocketError.fromWebSocketChannelError(
|
||||
WebSocketChannelException error) {
|
||||
final message = error.message ?? '';
|
||||
return StreamWebSocketError(message);
|
||||
}
|
||||
|
||||
///
|
||||
int? get code => data?.code;
|
||||
|
||||
///
|
||||
ChatErrorCode? get errorCode {
|
||||
final code = this.code;
|
||||
if (code == null) return null;
|
||||
return chatErrorCodeFromCode(code);
|
||||
}
|
||||
|
||||
/// Response body. please refer to [ErrorResponse].
|
||||
final ErrorResponse? data;
|
||||
|
||||
///
|
||||
bool get isRetriable => data == null;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [...super.props, code];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var params = 'message: $message';
|
||||
if (data != null) params += ', data: $data';
|
||||
return 'WebSocketError($params)';
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class StreamChatNetworkError extends StreamChatError {
|
||||
///
|
||||
StreamChatNetworkError(
|
||||
ChatErrorCode errorCode, {
|
||||
int? statusCode,
|
||||
this.data,
|
||||
}) : code = errorCode.code,
|
||||
statusCode = statusCode ?? data?.statusCode,
|
||||
super(errorCode.message);
|
||||
|
||||
///
|
||||
StreamChatNetworkError.raw({
|
||||
required this.code,
|
||||
required String message,
|
||||
this.statusCode,
|
||||
this.data,
|
||||
}) : super(message);
|
||||
|
||||
///
|
||||
factory StreamChatNetworkError.fromDioError(DioError error) {
|
||||
final response = error.response;
|
||||
ErrorResponse? errorResponse;
|
||||
final data = response?.data;
|
||||
if (data != null) {
|
||||
errorResponse = ErrorResponse.fromJson(data);
|
||||
}
|
||||
return StreamChatNetworkError.raw(
|
||||
code: errorResponse?.code ?? -1,
|
||||
message:
|
||||
errorResponse?.message ?? response?.statusMessage ?? error.message,
|
||||
statusCode: errorResponse?.statusCode ?? response?.statusCode,
|
||||
data: errorResponse,
|
||||
)..stackTrace = error.stackTrace;
|
||||
}
|
||||
|
||||
/// Error code
|
||||
final int code;
|
||||
|
||||
/// HTTP status code
|
||||
final int? statusCode;
|
||||
|
||||
/// Response body. please refer to [ErrorResponse].
|
||||
final ErrorResponse? data;
|
||||
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
///
|
||||
set stackTrace(StackTrace? stack) => _stackTrace = stack;
|
||||
|
||||
///
|
||||
ChatErrorCode? get errorCode => chatErrorCodeFromCode(code);
|
||||
|
||||
///
|
||||
bool get isRetriable => data == null;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [...super.props, code, statusCode];
|
||||
|
||||
@override
|
||||
String toString({bool printStackTrace = false}) {
|
||||
var params = 'code: $code, message: $message';
|
||||
if (statusCode != null) params += ', statusCode: $statusCode';
|
||||
if (data != null) params += ', data: $data';
|
||||
var msg = 'StreamChatNetworkError($params)';
|
||||
|
||||
if (printStackTrace && _stackTrace != null) {
|
||||
msg += '\n$_stackTrace';
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// ignore_for_file: use_setters_to_change_properties
|
||||
|
||||
/// Handles the connection id of the websocket connection
|
||||
class ConnectionIdManager {
|
||||
/// Initialize a new connection id manager
|
||||
ConnectionIdManager({
|
||||
String? connectionId,
|
||||
}) : _connectionId = connectionId;
|
||||
|
||||
String? _connectionId;
|
||||
|
||||
/// Get the current connection id
|
||||
String? get connectionId => _connectionId;
|
||||
|
||||
/// True if there is a connection id
|
||||
bool get hasConnectionId => _connectionId != null;
|
||||
|
||||
/// Set the connection id
|
||||
void setConnectionId(String connectionId) {
|
||||
_connectionId = connectionId;
|
||||
}
|
||||
|
||||
/// Clear the connection id
|
||||
void reset() {
|
||||
_connectionId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_http_client.dart';
|
||||
import 'package:stream_chat/src/core/http/token.dart';
|
||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||
|
||||
/// Authentication interceptor that refreshes the token if
|
||||
/// an auth error is received
|
||||
class AuthInterceptor extends Interceptor {
|
||||
/// Initialize a new auth interceptor
|
||||
AuthInterceptor(this._client, this._tokenManager);
|
||||
|
||||
final StreamHttpClient _client;
|
||||
|
||||
/// The token manager used in the client
|
||||
final TokenManager _tokenManager;
|
||||
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
late Token token;
|
||||
try {
|
||||
token = await _tokenManager.loadToken();
|
||||
} catch (_) {
|
||||
final error = StreamChatNetworkError(ChatErrorCode.undefinedToken);
|
||||
final dioError = StreamChatDioError(
|
||||
error: error,
|
||||
requestOptions: options,
|
||||
);
|
||||
return handler.reject(dioError, true);
|
||||
}
|
||||
final params = {'user_id': token.userId};
|
||||
final headers = {
|
||||
'Authorization': token.rawValue,
|
||||
'stream-auth-type': token.authType.raw,
|
||||
};
|
||||
options..queryParameters.addAll(params)..headers.addAll(headers);
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioError err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
ErrorResponse? error;
|
||||
final data = err.response?.data;
|
||||
if (data != null) error = ErrorResponse.fromJson(data);
|
||||
if (error?.code == ChatErrorCode.tokenExpired.code) {
|
||||
if (_tokenManager.isStatic) return handler.next(err);
|
||||
_client.lock();
|
||||
await _tokenManager.loadToken(refresh: true);
|
||||
_client.unlock();
|
||||
try {
|
||||
final options = err.requestOptions;
|
||||
final response = await _client.request(
|
||||
options.path,
|
||||
cancelToken: options.cancelToken,
|
||||
data: options.data,
|
||||
onReceiveProgress: options.onReceiveProgress,
|
||||
onSendProgress: options.onSendProgress,
|
||||
queryParameters: options.queryParameters,
|
||||
options: Options(
|
||||
method: options.method,
|
||||
sendTimeout: options.sendTimeout,
|
||||
receiveTimeout: options.receiveTimeout,
|
||||
extra: options.extra,
|
||||
headers: options.headers,
|
||||
responseType: options.responseType,
|
||||
contentType: options.contentType,
|
||||
validateStatus: options.validateStatus,
|
||||
receiveDataWhenStatusError: options.receiveDataWhenStatusError,
|
||||
followRedirects: options.followRedirects,
|
||||
maxRedirects: options.maxRedirects,
|
||||
requestEncoder: options.requestEncoder,
|
||||
responseDecoder: options.responseDecoder,
|
||||
listFormat: options.listFormat,
|
||||
),
|
||||
);
|
||||
return handler.resolve(response);
|
||||
} on DioError catch (error) {
|
||||
return handler.next(error);
|
||||
}
|
||||
}
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
||||
|
||||
/// Interceptor that injects the connection id in the request params
|
||||
class ConnectionIdInterceptor extends Interceptor {
|
||||
///
|
||||
ConnectionIdInterceptor(this.connectionIdManager);
|
||||
|
||||
///
|
||||
final ConnectionIdManager connectionIdManager;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
if (connectionIdManager.hasConnectionId) {
|
||||
options.queryParameters.addAll({
|
||||
'connection_id': connectionIdManager.connectionId,
|
||||
});
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
// coverage:ignore-file
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// Step where we're logging
|
||||
enum InterceptStep {
|
||||
/// Request
|
||||
request,
|
||||
|
||||
/// Response
|
||||
response,
|
||||
|
||||
/// Error
|
||||
error,
|
||||
}
|
||||
|
||||
/// Function used to print the log
|
||||
typedef LogPrint = void Function(InterceptStep step, Object object);
|
||||
|
||||
void _defaultLogPrint(InterceptStep step, Object object) => print(object);
|
||||
|
||||
/// Interceptor dedicated to logging
|
||||
class LoggingInterceptor extends Interceptor {
|
||||
/// Initialize a new logging interceptor
|
||||
LoggingInterceptor({
|
||||
this.request = true,
|
||||
this.requestHeader = false,
|
||||
this.requestBody = true,
|
||||
this.responseHeader = false,
|
||||
this.responseBody = true,
|
||||
this.error = true,
|
||||
this.maxWidth = 120,
|
||||
this.compact = true,
|
||||
this.logPrint = _defaultLogPrint,
|
||||
});
|
||||
|
||||
/// Print request [Options]
|
||||
final bool request;
|
||||
|
||||
/// Print request header [Options.headers]
|
||||
final bool requestHeader;
|
||||
|
||||
/// Print request data [Options.data]
|
||||
final bool requestBody;
|
||||
|
||||
/// Print [Response.data]
|
||||
final bool responseBody;
|
||||
|
||||
/// Print [Response.headers]
|
||||
final bool responseHeader;
|
||||
|
||||
/// Print error message
|
||||
final bool error;
|
||||
|
||||
/// InitialTab count to logPrint json response
|
||||
static const int initialTab = 1;
|
||||
|
||||
/// 1 tab length
|
||||
static const String tabStep = ' ';
|
||||
|
||||
/// Print compact json response
|
||||
final bool compact;
|
||||
|
||||
/// Width size per logPrint
|
||||
final int maxWidth;
|
||||
|
||||
/// Log printer; defaults logPrint log to console.
|
||||
/// In flutter, you'd better use debugPrint.
|
||||
/// you can also write log in a file.
|
||||
void Function(InterceptStep step, Object object) logPrint;
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (request) {
|
||||
_printRequestHeader(_logPrintRequest, options);
|
||||
}
|
||||
if (requestHeader) {
|
||||
_printMapAsTable(
|
||||
_logPrintRequest,
|
||||
options.queryParameters,
|
||||
header: 'Query Parameters',
|
||||
);
|
||||
final requestHeaders = <String, Object?>{...options.headers};
|
||||
requestHeaders['contentType'] = options.contentType?.toString();
|
||||
requestHeaders['responseType'] = options.responseType.toString();
|
||||
requestHeaders['followRedirects'] = options.followRedirects;
|
||||
requestHeaders['connectTimeout'] = options.connectTimeout;
|
||||
requestHeaders['receiveTimeout'] = options.receiveTimeout;
|
||||
_printMapAsTable(_logPrintRequest, requestHeaders, header: 'Headers');
|
||||
_printMapAsTable(_logPrintRequest, options.extra, header: 'Extras');
|
||||
}
|
||||
if (requestBody && options.method != 'GET') {
|
||||
final dynamic data = options.data;
|
||||
if (data != null) {
|
||||
if (data is Map) {
|
||||
_printMapAsTable(
|
||||
_logPrintRequest,
|
||||
options.data as Map?,
|
||||
header: 'Body',
|
||||
);
|
||||
} else if (data is FormData) {
|
||||
final formDataMap = <String, dynamic>{}
|
||||
..addEntries(data.fields)
|
||||
..addEntries(data.files);
|
||||
_printMapAsTable(_logPrintRequest, formDataMap,
|
||||
header: 'Form data | ${data.boundary}');
|
||||
} else {
|
||||
_printBlock(_logPrintRequest, data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequest(options, handler);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioError err, ErrorInterceptorHandler handler) {
|
||||
if (error) {
|
||||
if (err.type == DioErrorType.response) {
|
||||
final uri = err.response?.requestOptions.uri;
|
||||
_printBoxed(
|
||||
_logPrintError,
|
||||
header:
|
||||
'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}',
|
||||
text: uri.toString(),
|
||||
);
|
||||
if (err.response != null && err.response?.data != null) {
|
||||
_logPrintError('╔ ${err.type.toString()}');
|
||||
_printResponse(_logPrintError, err.response!);
|
||||
}
|
||||
_printLine(_logPrintError, '╚');
|
||||
_logPrintError('');
|
||||
} else {
|
||||
_printBoxed(
|
||||
_logPrintError,
|
||||
header: 'DioError ║ ${err.type}',
|
||||
text: err.message,
|
||||
);
|
||||
_printRequestHeader(_logPrintError, err.requestOptions);
|
||||
}
|
||||
}
|
||||
super.onError(err, handler);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
_printResponseHeader(_logPrintResponse, response);
|
||||
if (responseHeader) {
|
||||
final responseHeaders = <String, String>{};
|
||||
response.headers
|
||||
.forEach((k, list) => responseHeaders[k] = list.toString());
|
||||
_printMapAsTable(_logPrintResponse, responseHeaders, header: 'Headers');
|
||||
}
|
||||
|
||||
if (responseBody) {
|
||||
_logPrintResponse('╔ Body');
|
||||
_logPrintResponse('║');
|
||||
_printResponse(_logPrintResponse, response);
|
||||
_logPrintResponse('║');
|
||||
_printLine(_logPrintResponse, '╚');
|
||||
}
|
||||
super.onResponse(response, handler);
|
||||
}
|
||||
|
||||
void _printBoxed(
|
||||
void Function(Object) logPrint, {
|
||||
String? header,
|
||||
String? text,
|
||||
}) {
|
||||
logPrint('');
|
||||
logPrint('╔╣ $header');
|
||||
logPrint('║ $text');
|
||||
_printLine(logPrint, '╚');
|
||||
}
|
||||
|
||||
void _printResponse(void Function(Object) logPrint, Response response) {
|
||||
if (response.data != null) {
|
||||
if (response.data is Map) {
|
||||
_printPrettyMap(logPrint, response.data as Map);
|
||||
} else if (response.data is List) {
|
||||
logPrint('║${_indent()}[');
|
||||
_printList(logPrint, response.data as List);
|
||||
logPrint('║${_indent()}[');
|
||||
} else {
|
||||
_printBlock(logPrint, response.data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _printResponseHeader(void Function(Object) logPrint, Response response) {
|
||||
final uri = response.requestOptions.uri;
|
||||
final method = response.requestOptions.method;
|
||||
_printBoxed(
|
||||
logPrint,
|
||||
header:
|
||||
'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}',
|
||||
text: uri.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
void _printRequestHeader(
|
||||
void Function(Object) logPrint, RequestOptions options) {
|
||||
final uri = options.uri;
|
||||
final method = options.method;
|
||||
_printBoxed(logPrint, header: 'Request ║ $method ', text: uri.toString());
|
||||
}
|
||||
|
||||
void _printLine(void Function(Object) logPrint,
|
||||
[String pre = '', String suf = '╝']) =>
|
||||
logPrint('$pre${'═' * maxWidth}$suf');
|
||||
|
||||
void _printKV(void Function(Object) logPrint, String? key, Object? v) {
|
||||
final pre = '╟ $key: ';
|
||||
final msg = v.toString();
|
||||
|
||||
if (pre.length + msg.length > maxWidth) {
|
||||
logPrint(pre);
|
||||
_printBlock(logPrint, msg);
|
||||
} else {
|
||||
logPrint('$pre$msg');
|
||||
}
|
||||
}
|
||||
|
||||
void _printBlock(void Function(Object) logPrint, String msg) {
|
||||
final lines = (msg.length / maxWidth).ceil();
|
||||
for (var i = 0; i < lines; ++i) {
|
||||
logPrint((i >= 0 ? '║ ' : '') +
|
||||
msg.substring(i * maxWidth,
|
||||
math.min<int>(i * maxWidth + maxWidth, msg.length)));
|
||||
}
|
||||
}
|
||||
|
||||
String _indent([int tabCount = initialTab]) => tabStep * tabCount;
|
||||
|
||||
void _printPrettyMap(
|
||||
void Function(Object) logPrint,
|
||||
Map data, {
|
||||
int tabs = initialTab,
|
||||
bool isListItem = false,
|
||||
bool isLast = false,
|
||||
}) {
|
||||
var _tabs = tabs;
|
||||
final isRoot = _tabs == initialTab;
|
||||
final initialIndent = _indent(_tabs);
|
||||
_tabs++;
|
||||
|
||||
if (isRoot || isListItem) logPrint('║$initialIndent{');
|
||||
|
||||
data.keys.toList().asMap().forEach((index, dynamic key) {
|
||||
final isLast = index == data.length - 1;
|
||||
dynamic value = data[key];
|
||||
if (value is String) {
|
||||
value = '"${value.toString().replaceAll(RegExp(r'(\r|\n)+'), " ")}"';
|
||||
}
|
||||
if (value is Map) {
|
||||
if (compact) {
|
||||
logPrint('║${_indent(_tabs)} $key: $value${!isLast ? ',' : ''}');
|
||||
} else {
|
||||
logPrint('║${_indent(_tabs)} $key: {');
|
||||
_printPrettyMap(logPrint, value, tabs: _tabs);
|
||||
}
|
||||
} else if (value is List) {
|
||||
if (compact) {
|
||||
logPrint('║${_indent(_tabs)} $key: ${value.toString()}');
|
||||
} else {
|
||||
logPrint('║${_indent(_tabs)} $key: [');
|
||||
_printList(logPrint, value, tabs: _tabs);
|
||||
logPrint('║${_indent(_tabs)} ]${isLast ? '' : ','}');
|
||||
}
|
||||
} else {
|
||||
final msg = value.toString().replaceAll('\n', '');
|
||||
final indent = _indent(_tabs);
|
||||
final linWidth = maxWidth - indent.length;
|
||||
if (msg.length + indent.length > linWidth) {
|
||||
final lines = (msg.length / linWidth).ceil();
|
||||
for (var i = 0; i < lines; ++i) {
|
||||
logPrint('║${_indent(_tabs)} ${msg.substring(
|
||||
i * linWidth,
|
||||
math.min<int>(i * linWidth + linWidth, msg.length),
|
||||
)}');
|
||||
}
|
||||
} else {
|
||||
logPrint('║${_indent(_tabs)} $key: $msg${!isLast ? ',' : ''}');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}');
|
||||
}
|
||||
|
||||
void _printList(
|
||||
void Function(Object) logPrint,
|
||||
List list, {
|
||||
int tabs = initialTab,
|
||||
}) {
|
||||
list.asMap().forEach((i, dynamic e) {
|
||||
final isLast = i == list.length - 1;
|
||||
if (e is Map) {
|
||||
if (compact) {
|
||||
logPrint('║${_indent(tabs)} $e${!isLast ? ',' : ''}');
|
||||
} else {
|
||||
_printPrettyMap(logPrint, e,
|
||||
tabs: tabs + 1, isListItem: true, isLast: isLast);
|
||||
}
|
||||
} else {
|
||||
logPrint('║${_indent(tabs + 2)} $e${isLast ? '' : ','}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _printMapAsTable(
|
||||
void Function(Object) logPrint,
|
||||
Map? map, {
|
||||
String? header,
|
||||
}) {
|
||||
if (map == null || map.isEmpty) return;
|
||||
logPrint('╔ $header ');
|
||||
map.forEach((dynamic key, dynamic value) =>
|
||||
_printKV(logPrint, key.toString(), value));
|
||||
_printLine(logPrint, '╚');
|
||||
}
|
||||
|
||||
void _logPrintRequest(Object object) =>
|
||||
logPrint(InterceptStep.request, object);
|
||||
|
||||
void _logPrintResponse(Object object) =>
|
||||
logPrint(InterceptStep.response, object);
|
||||
|
||||
void _logPrintError(Object object) => logPrint(InterceptStep.error, object);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
|
||||
/// Error class specific to StreamChat and Dio
|
||||
class StreamChatDioError extends DioError {
|
||||
/// Initialize a stream chat dio error
|
||||
StreamChatDioError({
|
||||
required this.error,
|
||||
required RequestOptions requestOptions,
|
||||
Response? response,
|
||||
DioErrorType type = DioErrorType.other,
|
||||
}) : super(
|
||||
error: error,
|
||||
requestOptions: requestOptions,
|
||||
response: response,
|
||||
type: type,
|
||||
);
|
||||
|
||||
@override
|
||||
final StreamChatNetworkError error;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
|
||||
import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
|
||||
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
|
||||
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
|
||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/location.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
|
||||
part 'stream_http_client_options.dart';
|
||||
|
||||
/// This is where we configure the base url, headers,
|
||||
/// query parameters and convenient methods for http verbs with error parsing.
|
||||
class StreamHttpClient {
|
||||
/// [StreamHttpClient] constructor
|
||||
StreamHttpClient(
|
||||
this.apiKey, {
|
||||
Dio? dio,
|
||||
StreamHttpClientOptions? options,
|
||||
TokenManager? tokenManager,
|
||||
ConnectionIdManager? connectionIdManager,
|
||||
Logger? logger,
|
||||
}) : _options = options ?? const StreamHttpClientOptions(),
|
||||
httpClient = dio ?? Dio() {
|
||||
httpClient
|
||||
..options.baseUrl = _options.baseUrl
|
||||
..options.receiveTimeout = _options.receiveTimeout.inMilliseconds
|
||||
..options.connectTimeout = _options.connectTimeout.inMilliseconds
|
||||
..options.queryParameters = {'api_key': apiKey}
|
||||
..options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Stream-Client': _options.userAgent,
|
||||
'Content-Encoding': 'application/gzip',
|
||||
}
|
||||
..interceptors.addAll([
|
||||
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
||||
if (connectionIdManager != null)
|
||||
ConnectionIdInterceptor(connectionIdManager),
|
||||
if (logger != null && logger.level != Level.OFF)
|
||||
LoggingInterceptor(
|
||||
requestHeader: true,
|
||||
logPrint: (step, message) {
|
||||
switch (step) {
|
||||
case InterceptStep.request:
|
||||
return logger.info(message);
|
||||
case InterceptStep.response:
|
||||
return logger.info(message);
|
||||
case InterceptStep.error:
|
||||
return logger.severe(message);
|
||||
}
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Your project Stream Chat api key.
|
||||
/// Find your API keys here https://getstream.io/dashboard/
|
||||
final String apiKey;
|
||||
|
||||
/// Your project Stream Chat ClientOptions
|
||||
final StreamHttpClientOptions _options;
|
||||
|
||||
/// [Dio] httpClient
|
||||
/// It's been chosen because it's easy to use
|
||||
/// and supports interesting features out of the box
|
||||
/// (Interceptors, Global configuration, FormData, File downloading etc.)
|
||||
@visibleForTesting
|
||||
final Dio httpClient;
|
||||
|
||||
/// Lock the current [StreamHttpClient] instance.
|
||||
///
|
||||
/// [StreamHttpClient] will enqueue the incoming request tasks instead
|
||||
/// send them directly when [interceptor.requestOptions] is locked.
|
||||
void lock() => httpClient.lock();
|
||||
|
||||
/// Unlock the current [StreamHttpClient] instance.
|
||||
///
|
||||
/// [StreamHttpClient] instance dequeue the request task。
|
||||
void unlock() => httpClient.unlock();
|
||||
|
||||
/// Clear the current [StreamHttpClient] instance waiting queue.
|
||||
void clear() => httpClient.clear();
|
||||
|
||||
/// Shuts down the [StreamHttpClient].
|
||||
///
|
||||
/// If [force] is `false` the [StreamHttpClient] will be kept alive
|
||||
/// until all active connections are done. If [force] is `true` any active
|
||||
/// connections will be closed to immediately release all resources. These
|
||||
/// closed connections will receive an error event to indicate that the client
|
||||
/// was shut down. In both cases trying to establish a new connection after
|
||||
/// calling [close] will throw an exception.
|
||||
void close({bool force = false}) => httpClient.close(force: force);
|
||||
|
||||
StreamChatNetworkError _parseError(DioError err) {
|
||||
StreamChatNetworkError error;
|
||||
// locally thrown dio error
|
||||
if (err is StreamChatDioError) {
|
||||
error = err.error;
|
||||
} else {
|
||||
// real network request dio error
|
||||
error = StreamChatNetworkError.fromDioError(err);
|
||||
}
|
||||
return error..stackTrace = err.stackTrace;
|
||||
}
|
||||
|
||||
/// Handy method to make http GET request with error parsing.
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handy method to make http POST request with error parsing.
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.post<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
data: data,
|
||||
options: Options(headers: headers),
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handy method to make http DELETE request with error parsing.
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.delete<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handy method to make http PATCH request with error parsing.
|
||||
Future<Response<T>> patch<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.patch<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
data: data,
|
||||
options: Options(headers: headers),
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handy method to make http PUT request with error parsing.
|
||||
Future<Response<T>> put<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.put<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
data: data,
|
||||
options: Options(headers: headers),
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handy method to post files with error parsing.
|
||||
Future<Response<T>> postFile<T>(
|
||||
String path,
|
||||
MultipartFile file, {
|
||||
Map<String, Object?>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({'file': file});
|
||||
final response = await post<T>(
|
||||
path,
|
||||
data: formData,
|
||||
queryParameters: queryParameters,
|
||||
headers: headers,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Handy method to make generic http request with error parsing.
|
||||
Future<Response<T>> request<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, Object?>? queryParameters,
|
||||
Options? options,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.request<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
part of 'stream_http_client.dart';
|
||||
|
||||
const _defaultBaseURL = 'https://chat-us-east-1.stream-io-api.com';
|
||||
|
||||
/// Client options to modify [StreamHttpClient]
|
||||
class StreamHttpClientOptions {
|
||||
/// Instantiates a new [StreamHttpClientOptions]
|
||||
const StreamHttpClientOptions({
|
||||
String? baseUrl,
|
||||
this.location,
|
||||
this.connectTimeout = const Duration(seconds: 6),
|
||||
this.receiveTimeout = const Duration(seconds: 6),
|
||||
}) : _baseUrl = baseUrl ?? _defaultBaseURL;
|
||||
|
||||
final String _baseUrl;
|
||||
|
||||
/// base url to use with client.
|
||||
String get baseUrl {
|
||||
if (location == null) return _baseUrl;
|
||||
const serviceName = 'chat';
|
||||
final locationName = location!.name;
|
||||
const baseDomainName = 'stream-io-api.com';
|
||||
return 'https://$serviceName-proxy-$locationName.$baseDomainName';
|
||||
}
|
||||
|
||||
/// data center to use with client
|
||||
final Location? location;
|
||||
|
||||
/// connect timeout, default to 6s
|
||||
final Duration connectTimeout;
|
||||
|
||||
/// received timeout, default to 6s
|
||||
final Duration receiveTimeout;
|
||||
|
||||
/// Get the current user agent
|
||||
String get userAgent => 'stream-chat-dart-client-'
|
||||
'${CurrentPlatform.name}-'
|
||||
'${PACKAGE_VERSION.split('+')[0]}';
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:jose/jose.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/utils.dart';
|
||||
|
||||
/// A function which can be used to request a Stream Chat API token from your
|
||||
/// own backend server
|
||||
typedef GuestTokenProvider = Future<String> Function(User user);
|
||||
|
||||
/// Authentication type
|
||||
enum AuthType {
|
||||
/// JWT token
|
||||
jwt,
|
||||
|
||||
/// Anonymous user
|
||||
anonymous,
|
||||
}
|
||||
|
||||
/// Extension for returning the AuthType as a string
|
||||
extension AuthTypeX on AuthType {
|
||||
/// Returns the AuthType as a string
|
||||
String get raw => {
|
||||
AuthType.jwt: 'jwt',
|
||||
AuthType.anonymous: 'anonymous',
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
/// Token designed to store the JWT and the user it is related to.
|
||||
class Token extends Equatable {
|
||||
const Token._({
|
||||
required this.rawValue,
|
||||
required this.userId,
|
||||
required this.authType,
|
||||
});
|
||||
|
||||
/// The token that can be used when user is unknown.
|
||||
/// Is used by `anonymous` token provider.
|
||||
factory Token.anonymous({String? userId}) => Token._(
|
||||
rawValue: '',
|
||||
userId: userId ?? randomId(),
|
||||
authType: AuthType.anonymous,
|
||||
);
|
||||
|
||||
/// Creates a [Token] instance from the provided [rawValue] if it's valid.
|
||||
factory Token.fromRawValue(String rawValue) {
|
||||
final jwtBody = JsonWebToken.unverified(rawValue);
|
||||
final userId = jwtBody.claims.getTyped<String>('user_id');
|
||||
assert(
|
||||
userId != null,
|
||||
'Invalid `token`, It should contain `user_id`',
|
||||
);
|
||||
return Token._(rawValue: rawValue, userId: userId!, authType: AuthType.jwt);
|
||||
}
|
||||
|
||||
/// The token which can be used during the development.
|
||||
/// Is used by `development(userId:)` token provider.
|
||||
factory Token.development(String userId) {
|
||||
const devSignature = 'devtoken';
|
||||
const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
||||
final payload = json.encode({'user_id': userId});
|
||||
final payloadBytes = utf8.encode(payload);
|
||||
final payloadB64 = base64.encode(payloadBytes);
|
||||
final jwt = '$header.$payloadB64.$devSignature';
|
||||
return Token._(rawValue: jwt, userId: userId, authType: AuthType.jwt);
|
||||
}
|
||||
|
||||
/// The token which designed to be used for guest users.
|
||||
static Future<Token> guest(User user, GuestTokenProvider provider) async {
|
||||
final rawToken = await provider(user);
|
||||
return Token.fromRawValue(rawToken);
|
||||
}
|
||||
|
||||
/// Authentication type of this token
|
||||
final AuthType authType;
|
||||
|
||||
/// String value of the token
|
||||
final String rawValue;
|
||||
|
||||
/// User id associated with this token
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [authType, rawValue, userId];
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:stream_chat/src/core/http/token.dart';
|
||||
|
||||
/// A function which can be used to request a Stream Chat API token from your
|
||||
/// own backend server.
|
||||
/// Function requires a single [userId].
|
||||
typedef TokenProvider = Future<String> Function(String userId);
|
||||
|
||||
/// Handles common token operations
|
||||
class TokenManager {
|
||||
/// Initialize a new token manager
|
||||
TokenManager({
|
||||
String? userId,
|
||||
Token? token,
|
||||
TokenProvider? tokenProvider,
|
||||
}) : _userId = userId,
|
||||
_token = token,
|
||||
_provider = tokenProvider;
|
||||
|
||||
String? _type;
|
||||
Token? _token;
|
||||
|
||||
TokenProvider? _provider;
|
||||
|
||||
String? _userId;
|
||||
|
||||
/// User id to which this TokenManager is configured to
|
||||
String? get userId => _userId;
|
||||
|
||||
/// True if it's a static token
|
||||
bool get isStatic => _type == 'static';
|
||||
|
||||
/// Set a token or a token provider
|
||||
Future<Token> setTokenOrProvider(
|
||||
String userId, {
|
||||
Token? token,
|
||||
TokenProvider? provider,
|
||||
}) async {
|
||||
assert(() {
|
||||
if (token == null && provider == null) {
|
||||
throw AssertionError('Provide at-least token or provider');
|
||||
}
|
||||
if (token != null && provider != null) {
|
||||
throw AssertionError("Can't set both token and provider");
|
||||
}
|
||||
return true;
|
||||
}(), '');
|
||||
|
||||
_userId = userId;
|
||||
|
||||
if (token != null) {
|
||||
_type = 'static';
|
||||
_token = token;
|
||||
}
|
||||
if (provider != null) {
|
||||
_type = 'provider';
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
return loadToken();
|
||||
}
|
||||
|
||||
/// Returns the token refreshing the existing one if [refresh] is true
|
||||
Future<Token> loadToken({bool refresh = false}) async {
|
||||
assert(
|
||||
_userId != null && _type != null,
|
||||
'Please call `setTokenOrProvider` before calling `loadToken`',
|
||||
);
|
||||
if (refresh || _token == null) {
|
||||
final rawValue = await _provider!(_userId!);
|
||||
_token = Token.fromRawValue(rawValue);
|
||||
}
|
||||
return _token!;
|
||||
}
|
||||
|
||||
/// Resets the token manager
|
||||
void reset() {
|
||||
_userId = null;
|
||||
_token = null;
|
||||
_provider = null;
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -2,9 +2,9 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/action.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/core/models/action.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'attachment.g.dart';
|
||||
@@ -49,11 +49,11 @@ class Attachment extends Equatable {
|
||||
/// Create a new instance from a json
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Create a new instance from a db data
|
||||
factory Attachment.fromData(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
_$AttachmentFromJson(Serializer.moveToExtraDataFromRoot(
|
||||
json, topLevelFields + dbSpecificTopLevelFields));
|
||||
|
||||
///The attachment type based on the URL resource. This can be: audio,
|
||||
@@ -122,7 +122,7 @@ class Attachment extends Equatable {
|
||||
final String id;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
'type',
|
||||
'title_link',
|
||||
@@ -145,7 +145,7 @@ class Attachment extends Equatable {
|
||||
];
|
||||
|
||||
/// Known db specific top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const dbSpecificTopLevelFields = [
|
||||
'id',
|
||||
'upload_state',
|
||||
@@ -154,12 +154,12 @@ class Attachment extends Equatable {
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() =>
|
||||
Serialization.moveFromExtraDataToRoot(_$AttachmentToJson(this))
|
||||
Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this))
|
||||
..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key));
|
||||
|
||||
/// Serialize to db data
|
||||
Map<String, dynamic> toData() =>
|
||||
Serialization.moveFromExtraDataToRoot(_$AttachmentToJson(this));
|
||||
Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this));
|
||||
|
||||
Attachment copyWith({
|
||||
String? id,
|
||||
+33
-2
@@ -1,9 +1,13 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart' show MultipartFile;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/util/extension.dart';
|
||||
|
||||
part 'attachment_file.freezed.dart';
|
||||
|
||||
part 'attachment_file.g.dart';
|
||||
|
||||
/// Union class to hold various [UploadState] of a attachment.
|
||||
@@ -58,14 +62,18 @@ String? _toString(Uint8List? bytes) {
|
||||
@JsonSerializable()
|
||||
class AttachmentFile {
|
||||
/// Creates a new [AttachmentFile] instance.
|
||||
const AttachmentFile({
|
||||
AttachmentFile({
|
||||
required this.size,
|
||||
this.path,
|
||||
this.name,
|
||||
this.bytes,
|
||||
}) : assert(
|
||||
}) : assert(
|
||||
path != null || bytes != null,
|
||||
'Either path or bytes should be != null',
|
||||
),
|
||||
assert(
|
||||
!CurrentPlatform.isWeb || bytes != null,
|
||||
'File by path is not supported in web, Please provide bytes',
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
@@ -95,4 +103,27 @@ class AttachmentFile {
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
|
||||
|
||||
/// Converts this into a [MultipartFile]
|
||||
Future<MultipartFile> toMultipartFile() async {
|
||||
final filename = path?.split('/').last ?? name;
|
||||
final mimeType = filename?.mimeType;
|
||||
|
||||
late MultipartFile multiPartFile;
|
||||
|
||||
if (CurrentPlatform.isWeb) {
|
||||
multiPartFile = MultipartFile.fromBytes(
|
||||
bytes!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
} else {
|
||||
multiPartFile = await MultipartFile.fromFile(
|
||||
path!,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
}
|
||||
return multiPartFile;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/command.dart';
|
||||
import 'package:stream_chat/src/core/models/command.dart';
|
||||
|
||||
part 'channel_config.g.dart';
|
||||
|
||||
+15
-16
@@ -1,7 +1,7 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/channel_config.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_config.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'channel_model.g.dart';
|
||||
|
||||
@@ -37,7 +37,7 @@ class ChannelModel {
|
||||
/// Create a new instance from a json
|
||||
factory ChannelModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelModelFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// The id of this channel
|
||||
final String id;
|
||||
@@ -46,15 +46,15 @@ class ChannelModel {
|
||||
final String type;
|
||||
|
||||
/// The cid of this channel
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String cid;
|
||||
|
||||
/// The channel configuration data
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final ChannelConfig config;
|
||||
|
||||
/// The user that created this channel
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User? createdBy;
|
||||
|
||||
/// True if this channel is frozen
|
||||
@@ -62,24 +62,23 @@ class ChannelModel {
|
||||
final bool frozen;
|
||||
|
||||
/// The date of the last message
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? lastMessageAt;
|
||||
|
||||
/// The date of channel creation
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// The date of the last channel update
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// The date of channel deletion
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? deletedAt;
|
||||
|
||||
/// The count of this channel members
|
||||
@JsonKey(
|
||||
includeIfNull: false, toJson: Serialization.readOnly, defaultValue: 0)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0)
|
||||
final int memberCount;
|
||||
|
||||
/// Map of custom channel extraData
|
||||
@@ -90,11 +89,11 @@ class ChannelModel {
|
||||
final Map<String, Object?> extraData;
|
||||
|
||||
/// The team the channel belongs to
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String? team;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
'type',
|
||||
@@ -115,7 +114,7 @@ class ChannelModel {
|
||||
extraData.containsKey('name') ? extraData['name']! as String : cid;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$ChannelModelToJson(this),
|
||||
);
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/member.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/read.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/member.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/models/read.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'channel_state.g.dart';
|
||||
|
||||
+18
-21
@@ -1,7 +1,7 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
part 'event.g.dart';
|
||||
@@ -10,11 +10,11 @@ part 'event.g.dart';
|
||||
@JsonSerializable()
|
||||
class Event {
|
||||
/// Constructor used for json serialization
|
||||
const Event({
|
||||
this.type,
|
||||
Event({
|
||||
this.type = 'local.event',
|
||||
this.cid,
|
||||
this.connectionId,
|
||||
this.createdAt,
|
||||
DateTime? createdAt,
|
||||
this.me,
|
||||
this.user,
|
||||
this.message,
|
||||
@@ -29,18 +29,18 @@ class Event {
|
||||
this.parentId,
|
||||
this.extraData = const {},
|
||||
this.isLocal = true,
|
||||
});
|
||||
}) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Event.fromJson(Map<String, dynamic> json) =>
|
||||
_$EventFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
_$EventFromJson(Serializer.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
));
|
||||
|
||||
/// The type of the event
|
||||
/// [EventType] contains some predefined constant types
|
||||
final String? type;
|
||||
final String type;
|
||||
|
||||
/// The channel cid to which the event belongs
|
||||
final String? cid;
|
||||
@@ -55,7 +55,7 @@ class Event {
|
||||
final String? connectionId;
|
||||
|
||||
/// The date of creation of the event
|
||||
final DateTime? createdAt;
|
||||
final DateTime createdAt;
|
||||
|
||||
/// User object of the health check user
|
||||
final OwnUser? me;
|
||||
@@ -96,7 +96,7 @@ class Event {
|
||||
final Map<String, Object?> extraData;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static final topLevelFields = [
|
||||
'type',
|
||||
'cid',
|
||||
@@ -118,7 +118,7 @@ class Event {
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$EventToJson(this),
|
||||
);
|
||||
|
||||
@@ -160,11 +160,14 @@ class Event {
|
||||
channelType: channelType ?? this.channelType,
|
||||
parentId: parentId ?? this.parentId,
|
||||
extraData: extraData ?? this.extraData,
|
||||
isLocal: isLocal,
|
||||
);
|
||||
}
|
||||
|
||||
/// The channel embedded in the event object
|
||||
@JsonSerializable()
|
||||
@JsonSerializable(
|
||||
createToJson: false,
|
||||
)
|
||||
class EventChannel extends ChannelModel {
|
||||
/// Constructor used for json serialization
|
||||
EventChannel({
|
||||
@@ -198,7 +201,7 @@ class EventChannel extends ChannelModel {
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory EventChannel.fromJson(Map<String, dynamic> json) =>
|
||||
_$EventChannelFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
_$EventChannelFromJson(Serializer.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
));
|
||||
@@ -207,15 +210,9 @@ class EventChannel extends ChannelModel {
|
||||
final List<Member>? members;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static final topLevelFields = [
|
||||
'members',
|
||||
...ChannelModel.topLevelFields,
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$EventChannelToJson(this),
|
||||
);
|
||||
}
|
||||
+2
-28
@@ -8,7 +8,7 @@ part of 'event.dart';
|
||||
|
||||
Event _$EventFromJson(Map<String, dynamic> json) {
|
||||
return Event(
|
||||
type: json['type'] as String?,
|
||||
type: json['type'] as String,
|
||||
cid: json['cid'] as String?,
|
||||
connectionId: json['connection_id'] as String?,
|
||||
createdAt: json['created_at'] == null
|
||||
@@ -49,7 +49,7 @@ Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
|
||||
'channel_id': instance.channelId,
|
||||
'channel_type': instance.channelType,
|
||||
'connection_id': instance.connectionId,
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'me': instance.me?.toJson(),
|
||||
'user': instance.user?.toJson(),
|
||||
'message': instance.message?.toJson(),
|
||||
@@ -89,29 +89,3 @@ EventChannel _$EventChannelFromJson(Map<String, dynamic> json) {
|
||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$EventChannelToJson(EventChannel instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('cid', readonly(instance.cid));
|
||||
writeNotNull('config', readonly(instance.config));
|
||||
writeNotNull('created_by', readonly(instance.createdBy));
|
||||
val['frozen'] = instance.frozen;
|
||||
writeNotNull('last_message_at', readonly(instance.lastMessageAt));
|
||||
writeNotNull('created_at', readonly(instance.createdAt));
|
||||
writeNotNull('updated_at', readonly(instance.updatedAt));
|
||||
writeNotNull('deleted_at', readonly(instance.deletedAt));
|
||||
writeNotNull('member_count', readonly(instance.memberCount));
|
||||
val['extra_data'] = instance.extraData;
|
||||
val['members'] = instance.members?.map((e) => e.toJson()).toList();
|
||||
return val;
|
||||
}
|
||||
+18
-2
@@ -1,12 +1,13 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'member.g.dart';
|
||||
|
||||
/// The class that contains the information about the user membership
|
||||
/// in a channel
|
||||
@JsonSerializable()
|
||||
class Member {
|
||||
class Member extends Equatable {
|
||||
/// Constructor used for json serialization
|
||||
Member({
|
||||
this.user,
|
||||
@@ -98,4 +99,19 @@ class Member {
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$MemberToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
user,
|
||||
inviteAcceptedAt,
|
||||
inviteRejectedAt,
|
||||
invited,
|
||||
role,
|
||||
userId,
|
||||
isModerator,
|
||||
banned,
|
||||
shadowBanned,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
+27
-27
@@ -1,9 +1,9 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/attachment.dart';
|
||||
import 'package:stream_chat/src/models/reaction.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment.dart';
|
||||
import 'package:stream_chat/src/core/models/reaction.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
@@ -80,7 +80,7 @@ class Message extends Equatable {
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// The message ID. This is either created by Stream or set client side when
|
||||
/// the message is added.
|
||||
@@ -96,7 +96,7 @@ class Message extends Equatable {
|
||||
/// The message type
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
toJson: Serializer.readOnly,
|
||||
defaultValue: 'regular',
|
||||
)
|
||||
final String type;
|
||||
@@ -111,43 +111,43 @@ class Message extends Equatable {
|
||||
|
||||
/// The list of user mentioned in the message
|
||||
@JsonKey(
|
||||
toJson: Serialization.userIds,
|
||||
toJson: User.toIds,
|
||||
defaultValue: [],
|
||||
)
|
||||
final List<User> mentionedUsers;
|
||||
|
||||
/// A map describing the count of number of every reaction
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final Map<String, int>? reactionCounts;
|
||||
|
||||
/// A map describing the count of score of every reaction
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final Map<String, int>? reactionScores;
|
||||
|
||||
/// The latest reactions to the message created by any user.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final List<Reaction>? latestReactions;
|
||||
|
||||
/// The reactions added to the message by the current user.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final List<Reaction>? ownReactions;
|
||||
|
||||
/// The ID of the parent message, if the message is a thread reply.
|
||||
final String? parentId;
|
||||
|
||||
/// A quoted reply message
|
||||
@JsonKey(toJson: Serialization.readOnly)
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final Message? quotedMessage;
|
||||
|
||||
/// The ID of the quoted message, if the message is a quoted reply.
|
||||
final String? quotedMessageId;
|
||||
|
||||
/// Reserved field indicating the number of replies for this message.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final int? replyCount;
|
||||
|
||||
/// Reserved field indicating the thread participants for this message.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final List<User>? threadParticipants;
|
||||
|
||||
/// Check if this message needs to show in the channel.
|
||||
@@ -160,25 +160,25 @@ class Message extends Equatable {
|
||||
/// If true the message is shadowed
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
toJson: Serializer.readOnly,
|
||||
defaultValue: false,
|
||||
)
|
||||
final bool shadowed;
|
||||
|
||||
/// A used command name.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String? command;
|
||||
|
||||
/// Reserved field indicating when the message was created.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Reserved field indicating when the message was updated last time.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// User who sent the message
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User? user;
|
||||
|
||||
/// If true the message is pinned
|
||||
@@ -186,7 +186,7 @@ class Message extends Equatable {
|
||||
final bool pinned;
|
||||
|
||||
/// Reserved field indicating when the message was pinned
|
||||
@JsonKey(toJson: Serialization.readOnly)
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final DateTime? pinnedAt;
|
||||
|
||||
/// Reserved field indicating when the message will expire
|
||||
@@ -195,7 +195,7 @@ class Message extends Equatable {
|
||||
final DateTime? pinExpires;
|
||||
|
||||
/// Reserved field indicating who pinned the message
|
||||
@JsonKey(toJson: Serialization.readOnly)
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final User? pinnedBy;
|
||||
|
||||
/// Message custom extraData
|
||||
@@ -215,11 +215,11 @@ class Message extends Equatable {
|
||||
bool get isEphemeral => type == 'ephemeral';
|
||||
|
||||
/// Reserved field indicating when the message was deleted.
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? deletedAt;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
'text',
|
||||
@@ -251,7 +251,7 @@ class Message extends Equatable {
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$MessageToJson(this),
|
||||
);
|
||||
|
||||
@@ -403,14 +403,14 @@ class TranslatedMessage extends Message {
|
||||
/// Create a new instance from a json
|
||||
factory TranslatedMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$TranslatedMessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// A Map of
|
||||
final Map<String, String>? i18n;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static final topLevelFields = [
|
||||
'i18n',
|
||||
...Message.topLevelFields,
|
||||
@@ -418,7 +418,7 @@ class TranslatedMessage extends Message {
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$TranslatedMessageToJson(this),
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -84,7 +84,7 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
||||
|
||||
writeNotNull('type', readonly(instance.type));
|
||||
val['attachments'] = instance.attachments.map((e) => e.toJson()).toList();
|
||||
val['mentioned_users'] = Serialization.userIds(instance.mentionedUsers);
|
||||
val['mentioned_users'] = User.toIds(instance.mentionedUsers);
|
||||
writeNotNull('reaction_counts', readonly(instance.reactionCounts));
|
||||
writeNotNull('reaction_scores', readonly(instance.reactionScores));
|
||||
writeNotNull('latest_reactions', readonly(instance.latestReactions));
|
||||
+8
-11
@@ -1,12 +1,12 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
|
||||
part 'mute.g.dart';
|
||||
|
||||
/// The class that contains the information about a muted user
|
||||
@JsonSerializable()
|
||||
@JsonSerializable(createToJson: false)
|
||||
class Mute {
|
||||
/// Constructor used for json serialization
|
||||
Mute({
|
||||
@@ -20,21 +20,18 @@ class Mute {
|
||||
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
||||
|
||||
/// The user that performed the muting action
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User user;
|
||||
|
||||
/// The target user
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final ChannelModel channel;
|
||||
|
||||
/// The date in which the use was muted
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// The date of the last update
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$MuteToJson(this);
|
||||
}
|
||||
-16
@@ -14,19 +14,3 @@ Mute _$MuteFromJson(Map<String, dynamic> json) {
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MuteToJson(Mute instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('user', readonly(instance.user));
|
||||
writeNotNull('channel', readonly(instance.channel));
|
||||
writeNotNull('created_at', readonly(instance.createdAt));
|
||||
writeNotNull('updated_at', readonly(instance.updatedAt));
|
||||
return val;
|
||||
}
|
||||
+24
-28
@@ -1,14 +1,14 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/device.dart';
|
||||
import 'package:stream_chat/src/models/mute.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/device.dart';
|
||||
import 'package:stream_chat/src/core/models/mute.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
|
||||
part 'own_user.g.dart';
|
||||
|
||||
/// The class that defines the own user model
|
||||
/// This object can be found in [Event]
|
||||
@JsonSerializable()
|
||||
@JsonSerializable(createToJson: false)
|
||||
class OwnUser extends User {
|
||||
/// Constructor used for json serialization
|
||||
OwnUser({
|
||||
@@ -38,40 +38,42 @@ class OwnUser extends User {
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Create a new instance from [User] object
|
||||
factory OwnUser.fromUser(User user) => OwnUser(
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
lastActive: user.lastActive,
|
||||
online: user.online,
|
||||
banned: user.banned,
|
||||
extraData: user.extraData,
|
||||
);
|
||||
|
||||
/// List of user devices
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
defaultValue: <Device>[])
|
||||
@JsonKey(includeIfNull: false, defaultValue: <Device>[])
|
||||
final List<Device> devices;
|
||||
|
||||
/// List of users muted by the user
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
defaultValue: <Mute>[])
|
||||
@JsonKey(includeIfNull: false, defaultValue: <Mute>[])
|
||||
final List<Mute> mutes;
|
||||
|
||||
/// List of users muted by the user
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
defaultValue: <Mute>[])
|
||||
@JsonKey(includeIfNull: false, defaultValue: <Mute>[])
|
||||
final List<Mute> channelMutes;
|
||||
|
||||
/// Total unread messages by the user
|
||||
@JsonKey(
|
||||
includeIfNull: false, toJson: Serialization.readOnly, defaultValue: 0)
|
||||
@JsonKey(includeIfNull: false, defaultValue: 0)
|
||||
final int totalUnreadCount;
|
||||
|
||||
/// Total unread channels by the user
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false)
|
||||
final int? unreadChannels;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static final topLevelFields = [
|
||||
'devices',
|
||||
'mutes',
|
||||
@@ -80,10 +82,4 @@ class OwnUser extends User {
|
||||
'channel_mutes',
|
||||
...User.topLevelFields,
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$OwnUserToJson(this),
|
||||
);
|
||||
}
|
||||
-26
@@ -38,29 +38,3 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) {
|
||||
banned: json['banned'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$OwnUserToJson(OwnUser instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('role', readonly(instance.role));
|
||||
writeNotNull('created_at', readonly(instance.createdAt));
|
||||
writeNotNull('updated_at', readonly(instance.updatedAt));
|
||||
writeNotNull('last_active', readonly(instance.lastActive));
|
||||
writeNotNull('online', readonly(instance.online));
|
||||
writeNotNull('banned', readonly(instance.banned));
|
||||
val['extra_data'] = instance.extraData;
|
||||
writeNotNull('devices', readonly(instance.devices));
|
||||
writeNotNull('mutes', readonly(instance.mutes));
|
||||
writeNotNull('channel_mutes', readonly(instance.channelMutes));
|
||||
writeNotNull('total_unread_count', readonly(instance.totalUnreadCount));
|
||||
writeNotNull('unread_channels', readonly(instance.unreadChannels));
|
||||
return val;
|
||||
}
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'reaction.g.dart';
|
||||
|
||||
@@ -21,7 +21,7 @@ class Reaction {
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Reaction.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReactionFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
_$ReactionFromJson(Serializer.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
));
|
||||
@@ -33,11 +33,11 @@ class Reaction {
|
||||
final String type;
|
||||
|
||||
/// The date of the reaction
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// The user that sent the reaction
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User? user;
|
||||
|
||||
/// The score of the reaction (ie. number of reactions sent)
|
||||
@@ -45,7 +45,7 @@ class Reaction {
|
||||
final int score;
|
||||
|
||||
/// The userId that sent the reaction
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String? userId;
|
||||
|
||||
/// Reaction custom extraData
|
||||
@@ -66,7 +66,7 @@ class Reaction {
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$ReactionToJson(this),
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'read.g.dart';
|
||||
|
||||
+31
-13
@@ -1,11 +1,12 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
/// The class that defines the user model
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
class User extends Equatable {
|
||||
/// Constructor used for json serialization
|
||||
User({
|
||||
required this.id,
|
||||
@@ -21,11 +22,11 @@ class User {
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
factory User.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserFromJson(Serializer.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
'role',
|
||||
@@ -41,36 +42,36 @@ class User {
|
||||
final String id;
|
||||
|
||||
/// User role
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String? role;
|
||||
|
||||
/// User role
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serialization.readOnly,
|
||||
toJson: Serializer.readOnly,
|
||||
defaultValue: <String>[])
|
||||
final List<String> teams;
|
||||
|
||||
/// Date of user creation
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Date of last user update
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Date of last user connection
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? lastActive;
|
||||
|
||||
/// True if user is online
|
||||
@JsonKey(
|
||||
includeIfNull: false, toJson: Serialization.readOnly, defaultValue: false)
|
||||
includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false)
|
||||
final bool online;
|
||||
|
||||
/// True if user is banned from the chat
|
||||
@JsonKey(
|
||||
includeIfNull: false, toJson: Serialization.readOnly, defaultValue: false)
|
||||
includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false)
|
||||
final bool banned;
|
||||
|
||||
/// Map of custom user extraData
|
||||
@@ -92,13 +93,17 @@ class User {
|
||||
return id;
|
||||
}
|
||||
|
||||
/// List of users to list of userIds
|
||||
static List<String>? toIds(List<User>? users) =>
|
||||
users?.map((u) => u.id).toList();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is User && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$UserToJson(this),
|
||||
);
|
||||
|
||||
@@ -125,4 +130,17 @@ class User {
|
||||
banned: banned ?? this.banned,
|
||||
teams: teams ?? this.teams,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
role,
|
||||
teams,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastActive,
|
||||
online,
|
||||
banned,
|
||||
extraData,
|
||||
];
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector_stub.dart'
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.dart'
|
||||
if (dart.library.html) 'platform_detector_web.dart'
|
||||
if (dart.library.io) 'platform_detector_io.dart';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Version running on native systems
|
||||
PlatformType get currentPlatform {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Stub implementation
|
||||
PlatformType get currentPlatform {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Version running on web
|
||||
PlatformType get currentPlatform => PlatformType.web;
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
/// Useful extension functions for [Iterable]
|
||||
extension IterableX<T> on Iterable<T?> {
|
||||
/// Removes all the null values
|
||||
/// and converts `Iterable<T?>` into `Iterable<T>`
|
||||
Iterable<T> get withNullifyer => whereType();
|
||||
}
|
||||
|
||||
/// Useful extension functions for [Map]
|
||||
extension MapX<K, V> on Map<K?, V?> {
|
||||
/// Returns a new map with null keys or values removed
|
||||
Map<K, V> get nullProtected {
|
||||
final nullProtected = {...this}
|
||||
..removeWhere((key, value) => key == null || value == null);
|
||||
return nullProtected.cast();
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful extension functions for [String]
|
||||
extension StringX on String {
|
||||
/// returns the mime type from the passed file name.
|
||||
MediaType? get mimeType {
|
||||
if (toLowerCase().endsWith('heic')) {
|
||||
return MediaType.parse('image/heic');
|
||||
} else {
|
||||
final mimeType = lookupMimeType(this);
|
||||
if (mimeType == null) return null;
|
||||
return MediaType.parse(mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -1,18 +1,12 @@
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
/// Used to avoid to serialize properties to json
|
||||
// ignore: prefer_void_to_null
|
||||
Null readonly(_) => null;
|
||||
|
||||
/// Helper class for serialization to and from json
|
||||
class Serialization {
|
||||
class Serializer {
|
||||
/// Used to avoid to serialize properties to json
|
||||
static const Function readOnly = readonly;
|
||||
|
||||
/// List of users to list of userIds
|
||||
static List<String>? userIds(List<User>? users) =>
|
||||
users?.map((u) => u.id).toList();
|
||||
|
||||
/// Takes unknown json keys and puts them in the `extra_data` key
|
||||
static Map<String, dynamic> moveToExtraDataFromRoot(
|
||||
Map<String, dynamic> json,
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped
|
||||
// optimize the gzip compression for this alphabet.
|
||||
const _alphabet =
|
||||
'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
|
||||
|
||||
/// Generates a random String id
|
||||
/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js
|
||||
String randomId({int size = 21}) {
|
||||
var id = '';
|
||||
for (var i = 0; i < size; i++) {
|
||||
id += _alphabet[(math.Random().nextDouble() * 64).floor() | 0];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Creates a hash string from the passed [objects]
|
||||
String generateHash(List<Object?> objects) {
|
||||
final payload = json.encode(objects);
|
||||
final payloadBytes = utf8.encode(payload);
|
||||
final payloadB64 = base64.encode(payloadBytes);
|
||||
return payloadB64;
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:stream_chat/src/api/requests.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
import 'package:stream_chat/src/models/filter.dart';
|
||||
import 'package:stream_chat/src/models/member.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/reaction.dart';
|
||||
import 'package:stream_chat/src/models/read.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/extensions/iterable_extension.dart';
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/core/models/event.dart';
|
||||
import 'package:stream_chat/src/core/models/filter.dart';
|
||||
import 'package:stream_chat/src/core/models/member.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/models/reaction.dart';
|
||||
import 'package:stream_chat/src/core/models/read.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/extension.dart';
|
||||
|
||||
/// A simple client used for persisting chat data locally.
|
||||
abstract class ChatPersistenceClient {
|
||||
|
||||
@@ -3,6 +3,9 @@ class EventType {
|
||||
/// Indicates any type of events
|
||||
static const String any = '*';
|
||||
|
||||
///
|
||||
static const String healthCheck = 'health.check';
|
||||
|
||||
/// Event sent when a user starts typing a message
|
||||
static const String typingStart = 'typing.start';
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Exception related to api calls
|
||||
class ApiError extends Error {
|
||||
/// Creates a new ApiError instance using the response body and status code
|
||||
ApiError(this.body, this.status) : jsonData = _decode(body) {
|
||||
if (jsonData != null && jsonData!.containsKey('code')) {
|
||||
_code = jsonData!['code'];
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw body of the response
|
||||
final String? body;
|
||||
|
||||
/// Json parsed body
|
||||
final Map<String, dynamic>? jsonData;
|
||||
|
||||
/// Http status code of the response
|
||||
final int? status;
|
||||
|
||||
/// Stream specific error code
|
||||
int? get code => _code;
|
||||
int? _code;
|
||||
|
||||
static Map<String, dynamic>? _decode(String? body) {
|
||||
try {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
return json.decode(body);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ApiError &&
|
||||
runtimeType == other.runtimeType &&
|
||||
body == other.body &&
|
||||
jsonData == other.jsonData &&
|
||||
status == other.status &&
|
||||
_code == other._code;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
body.hashCode ^ jsonData.hashCode ^ status.hashCode ^ _code.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'ApiError{body: $body, jsonData: $jsonData, '
|
||||
'status: $status, code: $_code}';
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/// Useful extension functions for [Iterable]
|
||||
extension IterableX<T> on Iterable<T?> {
|
||||
/// Removes all the null values
|
||||
/// and converts `Iterable<T?>` into `Iterable<T>`
|
||||
Iterable<T> get withNullifyer => [
|
||||
for (final item in this)
|
||||
if (item != null) item
|
||||
];
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/// Useful extension functions for [Map]
|
||||
extension MapX<K, V> on Map<K, V> {
|
||||
/// Returns a new map with null keys or values removed
|
||||
Map<K, V> get nullProtected =>
|
||||
Map.from(this)..removeWhere((key, value) => key == null || value == null);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'dart:async' show Timer;
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Useful rate limiter extensions for [Function] class.
|
||||
extension RateLimit on Function {
|
||||
/// Converts this into a [Debounce] function.
|
||||
Debounce debounced(
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration? maxWait,
|
||||
}) =>
|
||||
Debounce(
|
||||
this,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: maxWait,
|
||||
);
|
||||
|
||||
/// Converts this into a [Throttle] function.
|
||||
Throttle throttled(
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) =>
|
||||
Throttle(
|
||||
this,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
/// TopLevel lambda to create [Debounce] functions.
|
||||
Debounce debounce(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration? maxWait,
|
||||
}) =>
|
||||
Debounce(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: maxWait,
|
||||
);
|
||||
|
||||
/// TopLevel lambda to create [Throttle] functions.
|
||||
Throttle throttle(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) =>
|
||||
Throttle(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
);
|
||||
|
||||
/// Creates a debounced function that delays invoking `func` until after `wait`
|
||||
/// milliseconds have elapsed since the last time the debounced function was
|
||||
/// invoked. The debounced function comes with a [Debounce.cancel] method to cancel
|
||||
/// delayed `func` invocations and a [Debounce.flush] method to immediately invoke them.
|
||||
/// Provide `leading` and/or `trailing` to indicate whether `func` should be
|
||||
/// invoked on the `leading` and/or `trailing` edge of the `wait` interval.
|
||||
/// The `func` is invoked with the last arguments provided to the [call]
|
||||
/// function. Subsequent calls to the debounced function return the result of
|
||||
/// the last `func` invocation.
|
||||
///
|
||||
/// **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
/// invoked on the trailing edge of the timeout only if the debounced function
|
||||
/// is invoked more than once during the `wait` timeout.
|
||||
///
|
||||
/// If `wait` is [Duration.zero] and `leading` is `false`,
|
||||
/// `func` invocation is deferred until the next tick.
|
||||
///
|
||||
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
/// for details over the differences between [Debounce] and [Throttle].
|
||||
///
|
||||
/// Some examples:
|
||||
///
|
||||
/// Avoid calling costly network calls when user is typing something.
|
||||
/// ```dart
|
||||
/// void fetchData(String query) async {
|
||||
/// final data = api.getData(query);
|
||||
/// doSomethingWithTheData(data);
|
||||
/// }
|
||||
///
|
||||
/// final debouncedFetchData = Debounce(
|
||||
/// fetchData,
|
||||
/// const Duration(milliseconds: 350),
|
||||
/// );
|
||||
///
|
||||
/// void onSearchQueryChanged(query) {
|
||||
/// debouncedFetchData(query);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Cancel the trailing debounced invocation.
|
||||
/// ```dart
|
||||
/// void dispose() {
|
||||
/// debounced.cancel();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Check for pending invocations.
|
||||
/// ```dart
|
||||
/// final status = debounced.isPending ? "Pending..." : "Ready";
|
||||
/// ```
|
||||
class Debounce {
|
||||
/// Creates a new instance of [Debounce].
|
||||
Debounce(
|
||||
this._func,
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration? maxWait,
|
||||
}) : _leading = leading,
|
||||
_trailing = trailing,
|
||||
_wait = wait.inMilliseconds,
|
||||
_maxing = maxWait != null {
|
||||
if (_maxing) {
|
||||
_maxWait = math.max(maxWait!.inMilliseconds, _wait);
|
||||
}
|
||||
}
|
||||
|
||||
final Function _func;
|
||||
final bool _leading;
|
||||
final bool _trailing;
|
||||
final int _wait;
|
||||
final bool _maxing;
|
||||
|
||||
late int _maxWait;
|
||||
List<Object?>? _lastArgs;
|
||||
Map<Symbol, Object>? _lastNamedArgs;
|
||||
Timer? _timer;
|
||||
int? _lastCallTime;
|
||||
Object? _result;
|
||||
int? _lastInvokeTime = 0;
|
||||
|
||||
Object? _invokeFunc(int? time) {
|
||||
final args = _lastArgs;
|
||||
final namedArgs = _lastNamedArgs;
|
||||
_lastArgs = _lastNamedArgs = null;
|
||||
_lastInvokeTime = time;
|
||||
return _result = Function.apply(_func, args, namedArgs);
|
||||
}
|
||||
|
||||
Timer _startTimer(Function pendingFunc, int wait) =>
|
||||
Timer(Duration(milliseconds: wait), pendingFunc as void Function());
|
||||
|
||||
bool _shouldInvoke(int time) {
|
||||
final timeSinceLastCall = time - (_lastCallTime ?? double.nan);
|
||||
final timeSinceLastInvoke = time - _lastInvokeTime!;
|
||||
|
||||
// Either this is the first call, activity has stopped and we're at the
|
||||
// trailing edge, the system time has gone backwards and we're treating
|
||||
// it as the trailing edge, or we've hit the `maxWait` limit.
|
||||
return _lastCallTime == null ||
|
||||
(timeSinceLastCall >= _wait) ||
|
||||
(timeSinceLastCall < 0) ||
|
||||
(_maxing && timeSinceLastInvoke >= _maxWait);
|
||||
}
|
||||
|
||||
Object? _trailingEdge(int time) {
|
||||
_timer = null;
|
||||
|
||||
// Only invoke if we have `lastArgs` which means `func` has been
|
||||
// debounced at least once.
|
||||
if (_trailing && _lastArgs != null) {
|
||||
return _invokeFunc(time);
|
||||
}
|
||||
_lastArgs = _lastNamedArgs = null;
|
||||
return _result;
|
||||
}
|
||||
|
||||
int _remainingWait(int time) {
|
||||
final timeSinceLastCall = time - _lastCallTime!;
|
||||
final timeSinceLastInvoke = time - _lastInvokeTime!;
|
||||
final timeWaiting = _wait - timeSinceLastCall;
|
||||
|
||||
return _maxing
|
||||
? math.min(timeWaiting, _maxWait - timeSinceLastInvoke)
|
||||
: timeWaiting;
|
||||
}
|
||||
|
||||
void _timerExpired() {
|
||||
final time = DateTime.now().millisecondsSinceEpoch;
|
||||
if (_shouldInvoke(time)) {
|
||||
_trailingEdge(time);
|
||||
} else {
|
||||
// Restart the timer.
|
||||
_timer = _startTimer(_timerExpired, _remainingWait(time));
|
||||
}
|
||||
}
|
||||
|
||||
Object? _leadingEdge(int? time) {
|
||||
// Reset any `maxWait` timer.
|
||||
_lastInvokeTime = time;
|
||||
// Start the timer for the trailing edge.
|
||||
_timer = _startTimer(_timerExpired, _wait);
|
||||
// Invoke the leading edge.
|
||||
return _leading ? _invokeFunc(time) : _result;
|
||||
}
|
||||
|
||||
/// Cancels all the remaining delayed functions.
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_lastInvokeTime = 0;
|
||||
_lastArgs = _lastNamedArgs = _lastCallTime = _timer = null;
|
||||
}
|
||||
|
||||
/// Immediately invokes all the remaining delayed functions.
|
||||
Object? flush() {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
return _timer == null ? _result : _trailingEdge(now);
|
||||
}
|
||||
|
||||
/// True if there are functions remaining to get invoked.
|
||||
bool get isPending => _timer != null;
|
||||
|
||||
/// Calls/invokes this class like a function.
|
||||
/// Pass [args] and [namedArgs] to be used while invoking [_func].
|
||||
Object? call(
|
||||
List<dynamic> args, {
|
||||
Map<Symbol, dynamic>? namedArgs,
|
||||
}) {
|
||||
final time = DateTime.now().millisecondsSinceEpoch;
|
||||
final isInvoking = _shouldInvoke(time);
|
||||
|
||||
_lastArgs = args;
|
||||
_lastNamedArgs = namedArgs as Map<Symbol, Object>?;
|
||||
_lastCallTime = time;
|
||||
|
||||
if (isInvoking) {
|
||||
if (_timer == null) {
|
||||
return _leadingEdge(_lastCallTime);
|
||||
}
|
||||
if (_maxing) {
|
||||
// Handle invocations in a tight loop.
|
||||
_timer = _startTimer(_timerExpired, _wait);
|
||||
return _invokeFunc(_lastCallTime);
|
||||
}
|
||||
}
|
||||
_timer ??= _startTimer(_timerExpired, _wait);
|
||||
return _result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a throttled function that only invokes `func` at most once per
|
||||
/// every `wait` milliseconds. The throttled function comes with a [Throttle.cancel]
|
||||
/// method to cancel delayed `func` invocations and a [Throttle.flush] method to
|
||||
/// immediately invoke them. Provide `leading` and/or `trailing` to indicate
|
||||
/// whether `func` should be invoked on the `leading` and/or `trailing` edge of the `wait` timeout.
|
||||
/// The `func` is invoked with the last arguments provided to the
|
||||
/// throttled function. Subsequent calls to the throttled function return the
|
||||
/// result of the last `func` invocation.
|
||||
///
|
||||
/// **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
/// invoked on the trailing edge of the timeout only if the throttled function
|
||||
/// is invoked more than once during the `wait` timeout.
|
||||
///
|
||||
/// If `wait` is [Duration.zero] and `leading` is `false`, `func` invocation is deferred
|
||||
/// until the next tick.
|
||||
///
|
||||
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
/// for details over the differences between [Throttle] and [Debounce].
|
||||
///
|
||||
/// Some examples:
|
||||
///
|
||||
/// Avoid excessively rebuilding UI progress while uploading data to server.
|
||||
/// ```dart
|
||||
/// void updateUI(Data data) {
|
||||
/// updateProgress(data);
|
||||
/// }
|
||||
///
|
||||
/// final throttledUpdateUI = Throttle(
|
||||
/// updateUI,
|
||||
/// const Duration(milliseconds: 350),
|
||||
/// );
|
||||
///
|
||||
/// void onUploadProgressChanged(progress) {
|
||||
/// throttledUpdateUI(progress);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Cancel the trailing throttled invocation.
|
||||
/// ```dart
|
||||
/// void dispose() {
|
||||
/// throttled.cancel();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Check for pending invocations.
|
||||
/// ```dart
|
||||
/// final status = throttled.isPending ? "Pending..." : "Ready";
|
||||
/// ```
|
||||
class Throttle {
|
||||
/// Creates a new instance of [Throttle]
|
||||
Throttle(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) : _debounce = Debounce(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: wait,
|
||||
);
|
||||
|
||||
final Debounce _debounce;
|
||||
|
||||
/// Cancels all the remaining delayed functions.
|
||||
void cancel() => _debounce.cancel();
|
||||
|
||||
/// Immediately invokes all the remaining delayed functions.
|
||||
Object? flush() => _debounce.flush();
|
||||
|
||||
/// True if there are functions remaining to get invoked.
|
||||
bool get isPending => _debounce.isPending;
|
||||
|
||||
/// Calls/invokes this class like a function.
|
||||
/// Pass [args] and [namedArgs] to be used while invoking `func`.
|
||||
Object? call(List<dynamic> args, {Map<Symbol, dynamic>? namedArgs}) =>
|
||||
_debounce.call(args, namedArgs: namedArgs);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
/// Useful extension functions for [String]
|
||||
extension StringX on String {
|
||||
/// Returns the mime type from the passed file name.
|
||||
http_parser.MediaType? get mimeType {
|
||||
if (toLowerCase().endsWith('heic')) {
|
||||
return http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
final mimeType = lookupMimeType(this);
|
||||
if (mimeType == null) {
|
||||
return null;
|
||||
}
|
||||
return http_parser.MediaType.parse(mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
///
|
||||
enum Location {
|
||||
///
|
||||
usEast,
|
||||
|
||||
///
|
||||
euWest,
|
||||
|
||||
///
|
||||
mumbai,
|
||||
|
||||
///
|
||||
sydney,
|
||||
|
||||
///
|
||||
singapore,
|
||||
}
|
||||
|
||||
///
|
||||
extension LocationX on Location {
|
||||
///
|
||||
String get name => {
|
||||
Location.usEast: 'us-east',
|
||||
Location.euWest: 'dublin',
|
||||
Location.mumbai: 'mumbai',
|
||||
Location.sydney: 'sydney',
|
||||
Location.singapore: 'singapore',
|
||||
}[this]!;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'dart:async';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
///
|
||||
class TimerHelper {
|
||||
final _uuid = const Uuid();
|
||||
late final _timers = <String, Timer>{};
|
||||
|
||||
///
|
||||
String setTimer(
|
||||
Duration duration,
|
||||
void Function() callback, {
|
||||
bool immediate = false,
|
||||
}) {
|
||||
final id = _uuid.v1();
|
||||
final timer = Timer(duration, callback);
|
||||
if (immediate) callback();
|
||||
_timers[id] = timer;
|
||||
return id;
|
||||
}
|
||||
|
||||
///
|
||||
String setPeriodicTimer(
|
||||
Duration duration,
|
||||
void Function(Timer) callback, {
|
||||
bool immediate = false,
|
||||
}) {
|
||||
final id = _uuid.v1();
|
||||
final timer = Timer.periodic(duration, callback);
|
||||
if (immediate) callback.call(timer);
|
||||
_timers[id] = timer;
|
||||
return id;
|
||||
}
|
||||
|
||||
///
|
||||
void cancelTimer(String id) {
|
||||
final timer = _timers.remove(id);
|
||||
return timer?.cancel();
|
||||
}
|
||||
|
||||
///
|
||||
void cancelAllTimers() {
|
||||
for (final t in _timers.values) {
|
||||
t.cancel();
|
||||
}
|
||||
_timers.clear();
|
||||
}
|
||||
|
||||
///
|
||||
bool get hasTimers => _timers.isNotEmpty;
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/core/error/error.dart';
|
||||
import 'package:stream_chat/src/ws/connection_status.dart';
|
||||
import 'package:stream_chat/src/core/http/token.dart';
|
||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||
import 'package:stream_chat/src/core/models/event.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/ws/timer_helper.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/status.dart' as status;
|
||||
|
||||
/// Typedef which exposes an [Event] as the only parameter.
|
||||
typedef EventHandler = void Function(Event);
|
||||
|
||||
/// Typedef used for connecting to a websocket. Method returns a
|
||||
/// [WebSocketChannel] and accepts a connection [url] and an optional
|
||||
/// [Iterable] of `protocols`.
|
||||
typedef WebSocketChannelProvider = WebSocketChannel Function(
|
||||
Uri uri, {
|
||||
Iterable<String>? protocols,
|
||||
});
|
||||
|
||||
/// A WebSocket connection that reconnects upon failure.
|
||||
class WebSocket with TimerHelper {
|
||||
/// Creates a new websocket
|
||||
/// To connect the WS call [connect]
|
||||
WebSocket({
|
||||
required this.apiKey,
|
||||
required this.baseUrl,
|
||||
required this.tokenManager,
|
||||
this.handler,
|
||||
Logger? logger,
|
||||
this.webSocketChannelProvider,
|
||||
this.reconnectionMonitorInterval = 10,
|
||||
this.healthCheckInterval = 20,
|
||||
this.reconnectionMonitorTimeout = 40,
|
||||
}) : _logger = logger;
|
||||
|
||||
///
|
||||
final String apiKey;
|
||||
|
||||
/// WS base url
|
||||
final String baseUrl;
|
||||
|
||||
///
|
||||
final TokenManager tokenManager;
|
||||
|
||||
/// Functions that will be called every time a new event is received from the
|
||||
/// connection
|
||||
final EventHandler? handler;
|
||||
|
||||
final Logger? _logger;
|
||||
|
||||
/// Connection function
|
||||
/// Used only for testing purpose
|
||||
@visibleForTesting
|
||||
final WebSocketChannelProvider? webSocketChannelProvider;
|
||||
|
||||
/// Interval of the reconnection monitor timer
|
||||
/// This checks that it received a new event in the last
|
||||
/// [reconnectionMonitorTimeout] seconds, otherwise it considers the
|
||||
/// connection unhealthy and reconnects the WS
|
||||
final int reconnectionMonitorInterval;
|
||||
|
||||
/// Interval of the health event sending timer
|
||||
/// This sends a health event every [healthCheckInterval] seconds in order to
|
||||
/// make the server aware that the client is still listening
|
||||
final int healthCheckInterval;
|
||||
|
||||
/// The timeout that uses the reconnection monitor timer to consider the
|
||||
/// connection unhealthy
|
||||
final int reconnectionMonitorTimeout;
|
||||
|
||||
User? _user;
|
||||
String? _connectionId;
|
||||
DateTime? _lastEventAt;
|
||||
WebSocketChannel? _webSocketChannel;
|
||||
StreamSubscription? _webSocketChannelSubscription;
|
||||
|
||||
///
|
||||
Completer<Event>? connectionCompleter;
|
||||
|
||||
///
|
||||
String? get connectionId => _connectionId;
|
||||
|
||||
final _connectionStatusController =
|
||||
BehaviorSubject.seeded(ConnectionStatus.disconnected);
|
||||
|
||||
set _connectionStatus(ConnectionStatus status) =>
|
||||
_connectionStatusController.add(status);
|
||||
|
||||
/// The current connection status value
|
||||
ConnectionStatus get connectionStatus => _connectionStatusController.value;
|
||||
|
||||
/// This notifies of connection status changes
|
||||
Stream<ConnectionStatus> get connectionStatusStream =>
|
||||
_connectionStatusController.stream.distinct();
|
||||
|
||||
void _initWebSocketChannel(Uri uri) {
|
||||
_logger?.info('Initiating connection with $baseUrl');
|
||||
if (_webSocketChannel != null) {
|
||||
_closeWebSocketChannel();
|
||||
}
|
||||
_webSocketChannel =
|
||||
webSocketChannelProvider?.call(uri) ?? WebSocketChannel.connect(uri);
|
||||
_subscribeToWebSocketChannel();
|
||||
}
|
||||
|
||||
void _closeWebSocketChannel() {
|
||||
_logger?.info('Closing connection with $baseUrl');
|
||||
if (_webSocketChannel != null) {
|
||||
_unsubscribeFromWebSocketChannel();
|
||||
_webSocketChannel?.sink.close(status.goingAway);
|
||||
_webSocketChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribeToWebSocketChannel() {
|
||||
_logger?.info('Started listening to $baseUrl');
|
||||
if (_webSocketChannelSubscription != null) {
|
||||
_unsubscribeFromWebSocketChannel();
|
||||
}
|
||||
_webSocketChannelSubscription = _webSocketChannel?.stream.listen(
|
||||
_onDataReceived,
|
||||
onError: _onConnectionError,
|
||||
onDone: _onConnectionClosed,
|
||||
);
|
||||
}
|
||||
|
||||
void _unsubscribeFromWebSocketChannel() {
|
||||
_logger?.info('Stopped listening to $baseUrl');
|
||||
if (_webSocketChannelSubscription != null) {
|
||||
_webSocketChannelSubscription?.cancel();
|
||||
_webSocketChannelSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> _buildUri({bool refreshToken = false}) async {
|
||||
final user = _user!;
|
||||
final token = await tokenManager.loadToken(refresh: refreshToken);
|
||||
final params = {
|
||||
'user_id': user.id,
|
||||
'user_details': user,
|
||||
'user_token': token.rawValue,
|
||||
'server_determines_connection_id': true,
|
||||
};
|
||||
final qs = {
|
||||
'json': jsonEncode(params),
|
||||
'api_key': apiKey,
|
||||
'authorization': token.rawValue,
|
||||
'stream-auth-type': token.authType.raw,
|
||||
};
|
||||
final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws';
|
||||
final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), '');
|
||||
return Uri(
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
pathSegments: ['connect'],
|
||||
queryParameters: qs,
|
||||
);
|
||||
}
|
||||
|
||||
bool _connectRequestInProgress = false;
|
||||
|
||||
/// Connect the WS using the parameters passed in the constructor
|
||||
Future<Event> connect(User user) async {
|
||||
if (_connectRequestInProgress) {
|
||||
throw const StreamWebSocketError('''
|
||||
You've called connect twice,
|
||||
can only attempt 1 connection at the time,
|
||||
''');
|
||||
}
|
||||
_connectRequestInProgress = true;
|
||||
_manuallyClosed = false;
|
||||
|
||||
_user = user;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
connectionCompleter = Completer<Event>();
|
||||
|
||||
final uri = await _buildUri();
|
||||
_initWebSocketChannel(uri);
|
||||
|
||||
return connectionCompleter!.future;
|
||||
}
|
||||
|
||||
int _reconnectAttempt = 0;
|
||||
bool _reconnectRequestInProgress = false;
|
||||
|
||||
void _reconnect({bool refreshToken = false}) async {
|
||||
_logger?.info('Retrying connection : $_reconnectAttempt');
|
||||
if (_reconnectRequestInProgress) return;
|
||||
_reconnectRequestInProgress = true;
|
||||
|
||||
_stopMonitoringEvents();
|
||||
// Closing any previously opened web-socket
|
||||
_closeWebSocketChannel();
|
||||
|
||||
_reconnectAttempt += 1;
|
||||
_connectionStatus = ConnectionStatus.connecting;
|
||||
|
||||
final delay = _getReconnectInterval(_reconnectAttempt);
|
||||
setTimer(
|
||||
Duration(milliseconds: delay),
|
||||
() async {
|
||||
final uri = await _buildUri(refreshToken: refreshToken);
|
||||
_initWebSocketChannel(uri);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// returns the reconnect interval based on `reconnectAttempt` in milliseconds
|
||||
int _getReconnectInterval(int reconnectAttempt) {
|
||||
// try to reconnect in 0.25-25 seconds
|
||||
// (random to spread out the load from failures)
|
||||
final max = math.min(500 + reconnectAttempt * 2000, 25000);
|
||||
final min = math.min(
|
||||
math.max(250, (reconnectAttempt - 1) * 2000),
|
||||
25000,
|
||||
);
|
||||
return (math.Random().nextDouble() * (max - min) + min).floor();
|
||||
}
|
||||
|
||||
void _startMonitoringEvents() {
|
||||
_logger?.info('Starting monitoring events');
|
||||
// cancel all previous timers
|
||||
cancelAllTimers();
|
||||
|
||||
_startHealthCheck();
|
||||
_startReconnectionMonitor();
|
||||
}
|
||||
|
||||
void _stopMonitoringEvents() {
|
||||
_logger?.info('Stopped monitoring events');
|
||||
// reset lastEvent
|
||||
_lastEventAt = null;
|
||||
|
||||
cancelAllTimers();
|
||||
}
|
||||
|
||||
void _handleConnectedEvent(Event event) {
|
||||
// updating connectionId and status
|
||||
_connectionId = event.connectionId;
|
||||
_connectionStatus = ConnectionStatus.connected;
|
||||
|
||||
_logger?.info('Connection successful: $_connectionId');
|
||||
|
||||
// notify user that connection is completed
|
||||
final completer = connectionCompleter;
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.complete(event);
|
||||
}
|
||||
|
||||
// start monitoring health-check events
|
||||
_startMonitoringEvents();
|
||||
}
|
||||
|
||||
void _handleHealthCheckEvent(Event event) {
|
||||
_logger?.info('HealthCheck received : ${event.connectionId}');
|
||||
|
||||
_connectionId = event.connectionId;
|
||||
_connectionStatus = ConnectionStatus.connected;
|
||||
}
|
||||
|
||||
void _handleStreamError(Map<String, Object?> errorResponse) {
|
||||
// resetting connect, reconnect request flag
|
||||
_resetRequestFlags();
|
||||
|
||||
final error = StreamWebSocketError.fromStreamError(errorResponse);
|
||||
final isTokenExpired = error.errorCode == ChatErrorCode.tokenExpired;
|
||||
if (isTokenExpired && !tokenManager.isStatic) {
|
||||
_logger?.warning('Connection failed, token expired');
|
||||
return _reconnect(refreshToken: true);
|
||||
}
|
||||
|
||||
_logger?.severe('Connection failed', error);
|
||||
|
||||
final completer = connectionCompleter;
|
||||
// complete with error if not yet completed
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
// complete the connection with error
|
||||
completer.completeError(error);
|
||||
// disconnect the web-socket connection
|
||||
return disconnect();
|
||||
}
|
||||
|
||||
return _reconnect();
|
||||
}
|
||||
|
||||
void _onDataReceived(dynamic data) {
|
||||
final jsonData = json.decode(data) as Map<String, Object?>;
|
||||
final error = jsonData['error'] as Map<String, Object?>?;
|
||||
if (error != null) return _handleStreamError(error);
|
||||
|
||||
// resetting connect, reconnect request flag
|
||||
_resetRequestFlags(resetAttempts: true);
|
||||
|
||||
Event? event;
|
||||
try {
|
||||
event = Event.fromJson(jsonData);
|
||||
} catch (_) {}
|
||||
|
||||
if (event == null) return;
|
||||
|
||||
_lastEventAt = DateTime.now();
|
||||
_logger?.info('Event received: ${event.type}');
|
||||
|
||||
if (event.type == EventType.healthCheck) {
|
||||
if (event.me != null) {
|
||||
_handleConnectedEvent(event);
|
||||
} else {
|
||||
_handleHealthCheckEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
return handler?.call(event);
|
||||
}
|
||||
|
||||
void _onConnectionError(error, [stacktrace]) {
|
||||
_logger?.warning('Error occurred', error, stacktrace);
|
||||
|
||||
StreamWebSocketError wsError;
|
||||
if (error is WebSocketChannelException) {
|
||||
wsError = StreamWebSocketError.fromWebSocketChannelError(error);
|
||||
} else {
|
||||
wsError = StreamWebSocketError(error.toString());
|
||||
}
|
||||
|
||||
final completer = connectionCompleter;
|
||||
// complete with error if not yet completed
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
// complete the connection with error
|
||||
completer.completeError(wsError);
|
||||
}
|
||||
|
||||
// resetting connect, reconnect request flag
|
||||
_resetRequestFlags();
|
||||
|
||||
_reconnect();
|
||||
}
|
||||
|
||||
bool _manuallyClosed = false;
|
||||
|
||||
void _onConnectionClosed() {
|
||||
_logger?.warning('Connection closed : $connectionId');
|
||||
|
||||
// resetting connect, reconnect request flag
|
||||
_resetRequestFlags();
|
||||
|
||||
// resetting connection
|
||||
_connectionId = null;
|
||||
|
||||
// check if we manually closed the connection
|
||||
if (_manuallyClosed) return;
|
||||
_reconnect();
|
||||
}
|
||||
|
||||
bool get _needsToReconnect {
|
||||
final lastEventAt = _lastEventAt;
|
||||
// means not yet connected or disconnected
|
||||
if (lastEventAt == null) return false;
|
||||
|
||||
// means we missed a health check
|
||||
final now = DateTime.now();
|
||||
return now.difference(lastEventAt).inSeconds > reconnectionMonitorTimeout;
|
||||
}
|
||||
|
||||
void _resetRequestFlags({bool resetAttempts = false}) {
|
||||
_connectRequestInProgress = false;
|
||||
_reconnectRequestInProgress = false;
|
||||
if (resetAttempts) _reconnectAttempt = 0;
|
||||
}
|
||||
|
||||
void _startReconnectionMonitor() {
|
||||
_logger?.info('Starting reconnection monitor');
|
||||
setPeriodicTimer(
|
||||
Duration(seconds: reconnectionMonitorInterval),
|
||||
(_) {
|
||||
final needsToReconnect = _needsToReconnect;
|
||||
_logger?.info('Needs to reconnect : $needsToReconnect');
|
||||
if (needsToReconnect) _reconnect();
|
||||
},
|
||||
immediate: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _startHealthCheck() {
|
||||
_logger?.info('Starting health check monitor');
|
||||
setPeriodicTimer(
|
||||
Duration(seconds: healthCheckInterval),
|
||||
(_) {
|
||||
_logger?.info('Sending Event: ${EventType.healthCheck}');
|
||||
final event = Event(
|
||||
type: EventType.healthCheck,
|
||||
connectionId: connectionId,
|
||||
);
|
||||
_webSocketChannel?.sink.add(jsonEncode(event));
|
||||
},
|
||||
immediate: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Disconnects the WS and releases eventual resources
|
||||
void disconnect() {
|
||||
if (connectionStatus == ConnectionStatus.disconnected) return;
|
||||
_connectionStatus = ConnectionStatus.disconnected;
|
||||
|
||||
_logger?.info('Disconnecting web-socket connection');
|
||||
|
||||
// resetting user
|
||||
_user = null;
|
||||
connectionCompleter = null;
|
||||
|
||||
_stopMonitoringEvents();
|
||||
|
||||
_manuallyClosed = true;
|
||||
_closeWebSocketChannel();
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,36 @@ export 'package:dio/src/multipart_file.dart';
|
||||
export 'package:dio/src/options.dart';
|
||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||
export 'package:logging/logging.dart' show Logger, Level;
|
||||
export 'package:rate_limiter/rate_limiter.dart';
|
||||
|
||||
export './src/api/channel.dart';
|
||||
export './src/api/connection_status.dart';
|
||||
export './src/api/requests.dart';
|
||||
export './src/api/requests.dart';
|
||||
export './src/api/responses.dart';
|
||||
export './src/attachment_file_uploader.dart' show AttachmentFileUploader;
|
||||
export './src/client.dart';
|
||||
export './src/core/api/attachment_file_uploader.dart'
|
||||
show AttachmentFileUploader;
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/responses.dart';
|
||||
export './src/core/api/stream_chat_api.dart' show PushProvider;
|
||||
export './src/core/error/error.dart';
|
||||
export './src/core/models/action.dart';
|
||||
export './src/core/models/attachment.dart';
|
||||
export './src/core/models/attachment_file.dart';
|
||||
export './src/core/models/channel_config.dart';
|
||||
export './src/core/models/channel_model.dart';
|
||||
export './src/core/models/channel_state.dart';
|
||||
export './src/core/models/command.dart';
|
||||
export './src/core/models/device.dart';
|
||||
export './src/core/models/event.dart';
|
||||
export './src/core/models/filter.dart' show Filter;
|
||||
export './src/core/models/member.dart';
|
||||
export './src/core/models/message.dart';
|
||||
export './src/core/models/mute.dart';
|
||||
export './src/core/models/own_user.dart';
|
||||
export './src/core/models/reaction.dart';
|
||||
export './src/core/models/read.dart';
|
||||
export './src/core/models/user.dart';
|
||||
export './src/core/util/extension.dart';
|
||||
export './src/db/chat_persistence_client.dart';
|
||||
export './src/event_type.dart';
|
||||
export './src/exceptions.dart';
|
||||
export './src/extensions/rate_limit.dart';
|
||||
export './src/extensions/string_extension.dart';
|
||||
export './src/models/action.dart';
|
||||
export './src/models/attachment.dart';
|
||||
export './src/models/attachment_file.dart';
|
||||
export './src/models/channel_config.dart';
|
||||
export './src/models/channel_model.dart';
|
||||
export './src/models/channel_state.dart';
|
||||
export './src/models/command.dart';
|
||||
export './src/models/device.dart';
|
||||
export './src/models/event.dart';
|
||||
export './src/models/filter.dart' show Filter;
|
||||
export './src/models/member.dart';
|
||||
export './src/models/message.dart';
|
||||
export './src/models/mute.dart';
|
||||
export './src/models/own_user.dart';
|
||||
export './src/models/reaction.dart';
|
||||
export './src/models/read.dart';
|
||||
export './src/models/user.dart';
|
||||
export './src/location.dart';
|
||||
export './src/ws/connection_status.dart';
|
||||
export 'src/client/channel.dart';
|
||||
export 'src/client/client.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/client/client.dart';
|
||||
|
||||
/// Current package version
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '2.0.0-nullsafety.2';
|
||||
const PACKAGE_VERSION = '2.0.0-nullsafety.7';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Configuration for https://pub.dev/packages/peanut
|
||||
directories:
|
||||
- example/web
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||
version: 2.0.0-nullsafety.2
|
||||
version: 2.0.0-nullsafety.7
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -15,10 +15,12 @@ dependencies:
|
||||
equatable: ^2.0.0
|
||||
freezed_annotation: ^0.14.0
|
||||
http_parser: ^4.0.0
|
||||
jose: ^0.3.2
|
||||
json_annotation: ^4.0.1
|
||||
logging: ^1.0.1
|
||||
meta: ^1.3.0
|
||||
mime: ^1.0.0
|
||||
rate_limiter: ^0.1.1
|
||||
rxdart: ^0.27.0
|
||||
uuid: ^3.0.4
|
||||
web_socket_channel: ^2.0.0
|
||||
@@ -28,4 +30,4 @@ dev_dependencies:
|
||||
freezed: ^0.14.1+3
|
||||
json_serializable: ^4.1.0
|
||||
mocktail: ^0.1.1
|
||||
test: ^1.16.8
|
||||
test: ^1.17.7
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user