diff --git a/.github/workflows/docusaurus.yml b/.github/workflows/docusaurus.yml new file mode 100644 index 00000000..f46fa893 --- /dev/null +++ b/.github/workflows/docusaurus.yml @@ -0,0 +1,20 @@ +name: docusaurus + +on: + push: + branches: + - master + - develop + paths: + - docusaurus/** +jobs: + push_docusaurus: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: push + uses: GetStream/push-stream-chat-docusaurus-action@main + with: + target-branch: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + env: + DOCUSAURUS_GH_TOKEN: ${{ secrets.DOCUSAURUS_GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 6817b269..0baaaff7 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -105,7 +105,7 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat/coverage/lcov.info - min_coverage: 40 + min_coverage: 80 - name: "Stream Chat Persistence Coverage Check" uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: @@ -120,4 +120,4 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 35 \ No newline at end of file + min_coverage: 67 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6143e72..0faad799 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ You can include the output of `flutter doctor --verbose` when filing an issue. Stream's Flutter code is kept in a single mono-repository consisting of multiple packages. Source code for each package can be found under the top-level `/packages` directory. -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/b1a202d3-44c8-4d1a-a5c3-583e7a3c4106/Screen_Shot_2021-03-31_at_4.13.52_PM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075243Z&X-Amz-Expires=86400&X-Amz-Signature=0582138dc957b784d4d505f6591cd3566cede2137fdf48ac546768822f344cab&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-03-31_at_4.13.52_PM.png%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/b1a202d3-44c8-4d1a-a5c3-583e7a3c4106/Screen_Shot_2021-03-31_at_4.13.52_PM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075243Z&X-Amz-Expires=86400&X-Amz-Signature=0582138dc957b784d4d505f6591cd3566cede2137fdf48ac546768822f344cab&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-03-31_at_4.13.52_PM.png%22) +Screen_Shot_2021-03-31_at_4 13 52_PM ### Project Structure 🧱 @@ -88,11 +88,11 @@ Before filing bugs, take a look at our existing backlog. For common bugs, there To quickly narrow down the amount of tickets on Github, try filtering based on the label that best suites the bug. -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/fbdb4e20-4914-416c-831d-6dee7d724501/Screen_Shot_2021-04-14_at_10.55.00_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075432Z&X-Amz-Expires=86400&X-Amz-Signature=e8c5a2a16de7e059bd1ee55f3d76736823e3fe0583bbb4ab8dd16bb9f792e33c&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_10.55.00_AM.png%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/fbdb4e20-4914-416c-831d-6dee7d724501/Screen_Shot_2021-04-14_at_10.55.00_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075432Z&X-Amz-Expires=86400&X-Amz-Signature=e8c5a2a16de7e059bd1ee55f3d76736823e3fe0583bbb4ab8dd16bb9f792e33c&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_10.55.00_AM.png%22) +![image](https://user-images.githubusercontent.com/20601437/124240983-9d9f6100-db1b-11eb-952f-3c0cc60a910e.png) Didn't find an existing issue? Go ahead and file a new bug using one of our pre-made issue templates. -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/02977615-c003-4b06-9d65-093ad95e6161/Screen_Shot_2021-04-14_at_10.56.17_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075509Z&X-Amz-Expires=86400&X-Amz-Signature=1153332c6bb3e54f69045c2ff47b3464039bbd4a8bd8a504ed7b2629eadb6ef8&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_10.56.17_AM.png%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/02977615-c003-4b06-9d65-093ad95e6161/Screen_Shot_2021-04-14_at_10.56.17_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075509Z&X-Amz-Expires=86400&X-Amz-Signature=1153332c6bb3e54f69045c2ff47b3464039bbd4a8bd8a504ed7b2629eadb6ef8&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_10.56.17_AM.png%22) +![image](https://user-images.githubusercontent.com/20601437/124241045-aee86d80-db1b-11eb-89eb-f4189019ac3e.png) Be sure to provide as much information as possible when filing bug reports. A good issue should have steps to reproduce and information on your development environment and expected behavior. @@ -108,7 +108,7 @@ To file a feature request, select the "Discussions" tab on our GitHub repo or [v Screenshots, sketches, and sample code are all welcomed! -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/4525f80a-e96f-4e05-8155-b4982df46c25/Screen_Shot_2021-04-14_at_11.08.48_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075528Z&X-Amz-Expires=86400&X-Amz-Signature=9bfbd045d99d3f3297cfd5d2885da85324d0f2da3ad272376680ca6913b050d5&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_11.08.48_AM.png%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/4525f80a-e96f-4e05-8155-b4982df46c25/Screen_Shot_2021-04-14_at_11.08.48_AM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075528Z&X-Amz-Expires=86400&X-Amz-Signature=9bfbd045d99d3f3297cfd5d2885da85324d0f2da3ad272376680ca6913b050d5&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Screen_Shot_2021-04-14_at_11.08.48_AM.png%22) +![image](https://user-images.githubusercontent.com/20601437/124241092-bc055c80-db1b-11eb-9205-7e3d7c157af1.png) Here are some common questions to answer when filing a feature request: @@ -130,7 +130,7 @@ Add any other context or screenshots about the feature request here. ## Pull Request 🎉 -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/fe7dad03-6edc-4ef4-8c3b-303853cd8aec/its-coding-time.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075543Z&X-Amz-Expires=86400&X-Amz-Signature=d49c488e4ec8dd15e10660be6b6fa13a49991771ebed93f395e251ae5a198150&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22its-coding-time.jpeg%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/fe7dad03-6edc-4ef4-8c3b-303853cd8aec/its-coding-time.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075543Z&X-Amz-Expires=86400&X-Amz-Signature=d49c488e4ec8dd15e10660be6b6fa13a49991771ebed93f395e251ae5a198150&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22its-coding-time.jpeg%22) +![image](https://user-images.githubusercontent.com/20601437/124241146-c7f11e80-db1b-11eb-9588-d9f578ec004a.png) Thank you for taking the time to submit a patch and contribute to our codebase. You rock! @@ -177,52 +177,159 @@ Develop is merged into master after the team performs various automated and QA t # Styleguides 💅 -![https://s3.us-west-2.amazonaws.com/secure.notion-static.com/c980959d-4ed2-4b93-97fc-016a9ded3f0f/d1d0f6c27141606521bfa6171bfe2a84.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075603Z&X-Amz-Expires=86400&X-Amz-Signature=94248827f187e1c2628ae1b0ff1aed1475325e2699acc89eb64145a2eb6d6968&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22d1d0f6c27141606521bfa6171bfe2a84.png%22](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/c980959d-4ed2-4b93-97fc-016a9ded3f0f/d1d0f6c27141606521bfa6171bfe2a84.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210423%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210423T075603Z&X-Amz-Expires=86400&X-Amz-Signature=94248827f187e1c2628ae1b0ff1aed1475325e2699acc89eb64145a2eb6d6968&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22d1d0f6c27141606521bfa6171bfe2a84.png%22) +![image](https://user-images.githubusercontent.com/20601437/124241186-d17a8680-db1b-11eb-9a21-3df305674ca9.png) We use style guides and lint checks to keep our code consistent and maintain best practices. Our team uses Dart's built-in analyzer for linting and enforcing code styles. The full list of analyzer rules can be found below. ## Dart lint rules 📖 -```jsx -include: package:pedantic/analysis_options.yaml - +```yaml analyzer: - enable-experiment: - - extension-methods exclude: - - lib/**/*.g.dart - - example/** + - packages/*/lib/**/*.g.dart + - packages/*/example/** + - packages/*/lib/src/emoji + - packages/*/lib/**/*.freezed.dart + - packages/*/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_init_to_null - - avoid_return_types_on_setters - - avoid_web_libraries_in_flutter - - await_only_futures - - camel_case_types + - avoid_relative_lib_imports + - avoid_slow_async_io + - avoid_types_as_parameter_names - cancel_subscriptions - close_sinks - control_flow_in_finally - - empty_constructor_bodies - empty_statements - hash_and_equals - - implementation_imports - - library_names - - non_constant_identifier_names - - package_names - - package_prefixed_library_names - - prefer_is_not_empty - - public_member_api_docs - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first + - 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 - - type_init_formals - - unnecessary_brace_in_string_interps - - unnecessary_getters_setters - 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 ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index 4b73086c..71604a87 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,10 +1,8 @@ analyzer: exclude: - packages/*/lib/**/*.g.dart - - packages/*/example/** - packages/*/lib/src/emoji - packages/*/lib/**/*.freezed.dart - - packages/*/test/** linter: rules: diff --git a/docusaurus/docs/Flutter/assets/channel_header.png b/docusaurus/docs/Flutter/assets/channel_header.png new file mode 100644 index 00000000..98f41c61 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_header.png differ diff --git a/docusaurus/docs/Flutter/assets/channel_header_custom_title.png b/docusaurus/docs/Flutter/assets/channel_header_custom_title.png new file mode 100644 index 00000000..65a9ee75 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_header_custom_title.png differ diff --git a/docusaurus/docs/Flutter/assets/channel_list_header.png b/docusaurus/docs/Flutter/assets/channel_list_header.png new file mode 100644 index 00000000..6f112db9 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_list_header.png differ diff --git a/docusaurus/docs/Flutter/assets/channel_list_header_custom_subtitle.png b/docusaurus/docs/Flutter/assets/channel_list_header_custom_subtitle.png new file mode 100644 index 00000000..3c779857 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_list_header_custom_subtitle.png differ diff --git a/docusaurus/docs/Flutter/assets/channel_list_view.png b/docusaurus/docs/Flutter/assets/channel_list_view.png new file mode 100644 index 00000000..a4390c2e Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_list_view.png differ diff --git a/docusaurus/docs/Flutter/assets/channel_preview.png b/docusaurus/docs/Flutter/assets/channel_preview.png new file mode 100644 index 00000000..39f5629b Binary files /dev/null and b/docusaurus/docs/Flutter/assets/channel_preview.png differ diff --git a/docusaurus/docs/Flutter/assets/chat_basics.png b/docusaurus/docs/Flutter/assets/chat_basics.png new file mode 100644 index 00000000..4eb0aa67 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/chat_basics.png differ diff --git a/docusaurus/docs/Flutter/assets/dashboard_firebase_enable.jpeg b/docusaurus/docs/Flutter/assets/dashboard_firebase_enable.jpeg new file mode 100644 index 00000000..e4696ec8 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/dashboard_firebase_enable.jpeg differ diff --git a/docusaurus/docs/Flutter/assets/dashboard_firebase_key.jpeg b/docusaurus/docs/Flutter/assets/dashboard_firebase_key.jpeg new file mode 100644 index 00000000..87243e91 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/dashboard_firebase_key.jpeg differ diff --git a/docusaurus/docs/Flutter/assets/dashboard_save_changes.jpeg b/docusaurus/docs/Flutter/assets/dashboard_save_changes.jpeg new file mode 100644 index 00000000..1c58a93e Binary files /dev/null and b/docusaurus/docs/Flutter/assets/dashboard_save_changes.jpeg differ diff --git a/docusaurus/docs/Flutter/assets/firebase_project_settings.jpeg b/docusaurus/docs/Flutter/assets/firebase_project_settings.jpeg new file mode 100644 index 00000000..4fbc6521 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/firebase_project_settings.jpeg differ diff --git a/docusaurus/docs/Flutter/assets/location_sharing_example.jpg b/docusaurus/docs/Flutter/assets/location_sharing_example.jpg new file mode 100644 index 00000000..7f220379 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/location_sharing_example.jpg differ diff --git a/docusaurus/docs/Flutter/assets/location_sharing_example_message.jpg b/docusaurus/docs/Flutter/assets/location_sharing_example_message.jpg new file mode 100644 index 00000000..707f1388 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/location_sharing_example_message.jpg differ diff --git a/docusaurus/docs/Flutter/assets/location_sharing_example_message_thumbnail.jpg b/docusaurus/docs/Flutter/assets/location_sharing_example_message_thumbnail.jpg new file mode 100644 index 00000000..e2b7d624 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/location_sharing_example_message_thumbnail.jpg differ diff --git a/docusaurus/docs/Flutter/assets/message_input.png b/docusaurus/docs/Flutter/assets/message_input.png new file mode 100644 index 00000000..d63cfbb3 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_input.png differ diff --git a/docusaurus/docs/Flutter/assets/message_input_change_position.png b/docusaurus/docs/Flutter/assets/message_input_change_position.png new file mode 100644 index 00000000..859107d2 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_input_change_position.png differ diff --git a/docusaurus/docs/Flutter/assets/message_input_quoted_message.png b/docusaurus/docs/Flutter/assets/message_input_quoted_message.png new file mode 100644 index 00000000..c1fda223 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_input_quoted_message.png differ diff --git a/docusaurus/docs/Flutter/assets/message_list_view.png b/docusaurus/docs/Flutter/assets/message_list_view.png new file mode 100644 index 00000000..4f9be344 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_list_view.png differ diff --git a/docusaurus/docs/Flutter/assets/message_list_view_pin.png b/docusaurus/docs/Flutter/assets/message_list_view_pin.png new file mode 100644 index 00000000..0b6f1c39 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_list_view_pin.png differ diff --git a/docusaurus/docs/Flutter/assets/message_list_view_threads.png b/docusaurus/docs/Flutter/assets/message_list_view_threads.png new file mode 100644 index 00000000..2c938766 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_list_view_threads.png differ diff --git a/docusaurus/docs/Flutter/assets/message_search_list_view.png b/docusaurus/docs/Flutter/assets/message_search_list_view.png new file mode 100644 index 00000000..b13f5bdc Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_search_list_view.png differ diff --git a/docusaurus/docs/Flutter/assets/sdk_title.png b/docusaurus/docs/Flutter/assets/sdk_title.png new file mode 100644 index 00000000..4d92e891 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/sdk_title.png differ diff --git a/docusaurus/docs/Flutter/assets/server_key.png b/docusaurus/docs/Flutter/assets/server_key.png new file mode 100644 index 00000000..f11d6fb5 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/server_key.png differ diff --git a/docusaurus/docs/Flutter/assets/swipe_channel.png b/docusaurus/docs/Flutter/assets/swipe_channel.png new file mode 100644 index 00000000..44f13ca5 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/swipe_channel.png differ diff --git a/docusaurus/docs/Flutter/assets/user_list_view.png b/docusaurus/docs/Flutter/assets/user_list_view.png new file mode 100644 index 00000000..4beffacc Binary files /dev/null and b/docusaurus/docs/Flutter/assets/user_list_view.png differ diff --git a/docusaurus/docs/Flutter/assets/using_theme.jpg b/docusaurus/docs/Flutter/assets/using_theme.jpg new file mode 100644 index 00000000..b835d931 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/using_theme.jpg differ diff --git a/docusaurus/docs/Flutter/basics/_category_.json b/docusaurus/docs/Flutter/basics/_category_.json new file mode 100644 index 00000000..76d33569 --- /dev/null +++ b/docusaurus/docs/Flutter/basics/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Introduction", + "position": 1 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/basics/choose_package.mdx b/docusaurus/docs/Flutter/basics/choose_package.mdx new file mode 100644 index 00000000..8be73f01 --- /dev/null +++ b/docusaurus/docs/Flutter/basics/choose_package.mdx @@ -0,0 +1,55 @@ +--- +id: choose_package +sidebar_position: 2 +title: Choosing The Right Flutter Package +--- + +### Why the SDK is split into different packages + +Different applications need different levels of customization and integration with the Stream Chat SDK. +To do this, the Flutter SDK is split into three different packages which build upon the last and give +varying levels of control to the developer. The higher level packages offer better compatibility out of the +box while the lower level SDKs offer fine grained control. There is also a separate package for persistence +which allows you persist data locally which works with all packages. + +### How do I choose? + +#### The case for stream_chat_flutter + +For the quickest way to integrate Stream Chat with your app, the UI SDK (`stream_chat_flutter`) is the +way to go. `stream_chat_flutter` contains prebuilt components that manage most operations like data +fetching, pagination, sending a message, and more. This ensures you have a nearly out-of-the-box +experience adding chat to your applications. It is also possible to use this in conjunction with +lower level operations of the SDK to get the best of both worlds. + +:::note +The package allows customization of components to a large extent making it easy to tweak the theme +to match your app colors and such. If you require any additional feature or customization, feel free +to request this through our support channels. +::: + +Summary: + +For the quickest and easiest way to add Chat to your app with prebuilt UI components, use stream_chat_flutter + + +#### The case for stream_chat_flutter_core + +If your application involves UI that does not fit in with the stream_chat_flutter components, stream_chat_flutter_core +strips away the UI associated with the components and provides the data fetching and manipulation +capabilities while supplying builders for UI. This allows you to implement your own UI and themes +completely independently while not worrying about writing functions for data and pagination. + +Summary: + +For implementing your own custom UI while not having to worry about lower level API calls, use stream_chat_flutter_core. + +#### The case for stream_chat + +The stream_chat package is the Low-level Client (LLC) of Stream Chat in Flutter. This package wraps +the underlying functionality of Stream Chat and allows the most customization in terms of UI, data, +and architecture. + +Summary: + +For the most control over the SDK and dealing with low level calls to the API, use stream_chat. diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx new file mode 100644 index 00000000..4c45855c --- /dev/null +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -0,0 +1,71 @@ +--- +slug: / +id: introduction +sidebar_position: 1 +title: About The Flutter SDK +--- +Exploring The Basics Of Stream Chat + +![](../assets/sdk_title.png) + +Stream Chat is a service that helps you easily build a full chat experience in your Flutter (and more) apps. + +This section of the documentation focuses on our Flutter SDK which helps you easily +ship high quality messaging experiences in apps and programs built with the [Flutter toolkit made +by Google](https://flutter.dev). + +The Stream Chat Flutter SDK comprises of four different packages to choose from ranging from ones +giving you complete control to ones that give you a rich out-of-the-box chat experience. + +The packages that make up the Stream Chat SDK are: + +1. Low Level Client (stream_chat): a pure Dart package that can be used on any Dart project. +It provides a low-level client to access the Stream Chat service. +2. Core (stream_chat_flutter_core): provides business logic to fetch common things required +for integrating Stream Chat into your application. +The core package allows more customisation and hence provides business logic but no UI components. +3. UI (stream_chat_flutter): this library includes both a low-level chat SDK and a set of +reusable and customisable UI components. +4. Persistence (stream_chat_persistence): provides a persistence client for fetching and +saving chat data locally. + +We recommend building prototypes using the full UI package since it contains UI widgets already +integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter) +is the fastest way to get up and running using Stream chat in your app. + +The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web +and Desktop. + +If you're building a very custom UI and would prefer a more lean package, +our [core package](https://pub.dev/packages/stream_chat_flutter) will be suited to this use case. Core allows you to build custom, +expressive UIs while retaining the benefits of our full Flutter SDK. +APIs for accessing and controlling users, sending messages, etc are seamlessly integrated into +this package and accessible via providers and builders. + +Before going into the docs, let's take a small detour to look at how the elements of Stream Chat are structured. + +There are two core elements in chat, Users and Channels. +Channels are groups of one or more users that can message each other. +In an app, you need to have a user connected to query channels. + +There is no specific distinction between a chat between two people and a group chat, +but there is a way to create a unique chat between a certain number of people by creating a distinct channel. + +![](../assets/chat_basics.png) + +In essence, a normal two-person chat would be a distinct channel created with two members (you cannot add or delete members in this channel), whereas a group created with two people would simply be a non distinct channel (possible to add or remove members). + +Note: It is also possible to add more than two people in a distinct channel which retains the same add/removal properties and resembles the Slack DMs where you can DM one or more people as well. + +In summary, if you were creating a Whatsapp-like app, the first screen would be a list of channels - which on opening would show a list of messages that were sent by the users in the Channel. + +While this is a simplistic overview of the service, the Flutter SDK handles the UI and more time consuming things (media upload, offline storage, theming, etc.) for you. + +Before reading the docs, consider trying our [online API tour](https://getstream.io/chat/get_started/), +it is a nice way to learn how the API works. +It's in-browser so Javascript-based but the ideas are pretty much the same as Dart. + +You may also like to look at the [Flutter tutorial](https://getstream.io/chat/flutter/tutorial/) +which focuses on using the UI package to get Stream Chat integrated into a Flutter app. + +Further sections break down each individual packages and explain several common operations. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/guides/_category_.json b/docusaurus/docs/Flutter/guides/_category_.json new file mode 100644 index 00000000..cb58ac0d --- /dev/null +++ b/docusaurus/docs/Flutter/guides/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 2 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx new file mode 100644 index 00000000..0c427713 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx @@ -0,0 +1,260 @@ +--- +id: adding_custom_attachments +sidebar_position: 4 +title: Adding Custom Attachments +--- + +Adding Your Own Types Of Attachments To A Message + +### Introduction + +Stream Chat supports attachment types like images, video and files by default. You can also add your +own types of attachments through the SDK such as location, audio, etc. + +This involves doing three things: + +1) Rendering the attachment thumbnail in the `MessageInput` + +2) Sending a message with the custom attachment + +3) Rendering the custom message attachment + +To do this, let's check out an example to add location sharing to Stream Chat. + +### Location Sharing + +Let's build an example of location sharing option in the app: + +![](../assets/location_sharing_example.jpg) + +* Show a "Share Location" button next to MessageInput Textfield. + +* When the user presses this button, it should fetch the current location coordinates of the user, and send a message on the channel as follows: + +```dart +Message( + text: 'This is my location', + attachments: [ + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': 'fetched_latitude', + 'longitude': 'fetched_longitude', + }, + ), + ], +) +``` + +For our example, we are going to use [geolocator](https://pub.dev/packages/geolocator) library. +Please check their [setup instructions](https://pub.dev/packages/geolocator) on their docs. + +NOTE: If you are testing on iOS simulator, you will need to set some dummy coordinates, as mentioned [here](https://stackoverflow.com/a/31238119/7489541). +Also don't forget to enable "location update" capability in background mode, from XCode. + +On the receiver end, `location` type attachment should be rendered in map view, in the `MessageListView`. +We are going to use [Google Static Maps API](https://developers.google.com/maps/documentation/maps-static/overview) to render the map in the message. +You can use other libraries as well such as [google_maps_flutter](https://pub.dev/packages/google_maps_flutter). + +First, we add a button which when clicked fetches and shares location into the `MessageInput`: + +```dart +MessageInput( + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + var channel = StreamChannel.of(context).channel; + var user = StreamChat.of(context).user; + + _determinePosition().then((value) { + channel.sendMessage( + Message( + text: 'This is my location', + attachments: [ + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ], + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], +), + +Future _determinePosition() async { + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return Future.error('Location services are disabled.'); + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.deniedForever) { + return Future.error( + 'Location permissions are permanently denied, we cannot request permissions.'); + } + + if (permission == LocationPermission.denied) { + return Future.error( + 'Location permissions are denied'); + } + } + + return await Geolocator.getCurrentPosition(); +} +``` + +Next, we build the Static Maps URL (Add your API key before using the code snippet): + +```dart + String _buildMapAttachment(String lat, String long) { + var baseURL = 'https://maps.googleapis.com/maps/api/staticmap?'; + var url = Uri( + scheme: 'https', + host: 'maps.googleapis.com', + port: 443, + path: '/maps/api/staticmap', + queryParameters: { + 'center': '${lat},${long}', + 'zoom': '15', + 'size': '600x300', + 'maptype': 'roadmap', + 'key': 'YOUR_API_KEY', + 'markers': 'color:red|${lat},${long}' + }); + + return url.toString(); + } +``` + +And then modify the MessageListView and tell it how to build a location attachment: + +```dart +MessageListView( + customAttachmentBuilders: { + 'location': (context, message, attachments) { + var attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, +), +``` + +This gives us the final location attachment: + +![](../assets/location_sharing_example_message.jpg) + +Additionally, you can also add a thumbnail if a message has a location attachment (unlike in this case, where we sent the message directly). + +To do this, we will: + +1) Add an attachment instead of sending a message + +2) Customize the `MessageInput` + +First, we add the attachment when the location button is clicked: + +```dart + GlobalKey _messageInputKey = GlobalKey(); + + MessageInput( + key: _messageInputKey, + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + _determinePosition().then((value) { + _messageInputKey.currentState.addAttachment( + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], + ), +``` + +After this, we can build the thumbnail: + +```dart +MessageInput( + key: _messageInputKey, + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + _determinePosition().then((value) { + _messageInputKey.currentState.addAttachment( + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], + attachmentThumbnailBuilders: { + 'location': (context, attachment) { + return Image.network( + _buildMapAttachment( + attachment.extraData['latitude'], + attachment.extraData['longitude'], + ), + ); + }, + }, +), +``` + +And we can see the thumbnails in the MessageInput: + +![](../assets/location_sharing_example_message_thumbnail.jpg) diff --git a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx new file mode 100644 index 00000000..21ec99ce --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx @@ -0,0 +1,6 @@ +--- +id: local_data_persistence +sidebar_position: 2 +title: Adding Local Data Persistence +--- + diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx new file mode 100644 index 00000000..529f03cb --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx @@ -0,0 +1,234 @@ +--- +id: adding_push_notifications +sidebar_position: 3 +title: Adding Push Notifications +--- + +Adding Push Notifications To Your Application + +### Introduction + +Push notifications are a core part of the experience for a messaging app. Users often need to be notified +of new messages and old notifications sometimes need to be updated silently as well. + +This guide details how to add push notifications to your app. + +Make sure to check [this section](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) of the docs to read about the push delivery logic. + +### Setup FCM + +To integrate push notifications in your Flutter app you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging). + + +Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to know how to set up the plugin for both Android and iOS. + + +Once that's done FCM should be able to send push notifications to your devices. + +### Integration with Stream + +#### Step 1 + +From the [Firebase Console](https://console.firebase.google.com/), select the project your app belongs to. + +#### Step 2 + +Click on the gear icon next to `Project Overview` and navigate to **Project settings** + +![](../assets/firebase_project_settings.jpeg) + +#### Step 3 + +Navigate to the `Cloud Messaging` tab + +#### Step 4 + +Under `Project Credentials`, locate the `Server key` and copy it + +![](../assets/server_key.png) + +#### Step 5 + +Upload the `Server Key` in your chat dashboard + +![](../assets/dashboard_firebase_enable.jpeg) + +![](../assets/dashboard_firebase_key.jpeg) + + +:::note +We are setting up the Android section, but this will work for both Android and iOS if you're using Firebase for both of them! +::: + +#### Step 6 + +Save your push notification settings changes + +![](../assets/dashboard_save_changes.jpeg) + +**OR** + +Upload the `Server Key` via API call using a backend SDK + +```js +await client.updateAppSettings({ + firebase_config: { + server_key: 'server_key', + notification_template: `{"message":{"notification":{"title":"New messages","body":"You have {{ unread_count }} new message(s) from {{ sender.name }}"},"android":{"ttl":"86400s","notification":{"click_action":"OPEN_ACTIVITY_1"}}}}`, + data_template: `{"sender":"{{ sender.id }}","channel":{"type": "{{ channel.type }}","id":"{{ channel.id }}"},"message":"{{ message.id }}"}` + }, +}); +``` + +### Registering a device at Stream Backend + +Once you configure Firebase server key and set it up on Stream dashboard a device that is supposed to receive push notifications needs to be registered at Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows: + +```dart +firebaseMessaging.onTokenRefresh.listen((token) { + client.addDevice(token, PushProvider.firebase); +}); +``` + +### Possible issues + + +We only send push notifications when the user doesn't have any active websocket connection (which is established when you call `client.connectUser`). If you set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) property of the StreamChat widget, when your app goes to background, your device will keep the ws connection alive for 1 minute, and so within this period, you won't receive any push notification. + +Make sure to read the [general push docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications go bad 😢 + +### Testing if Push Notifications are Setup Correctly + +If you're not sure if you've set up push notifications correctly (e.g. you don't always receive them, they work unreliably), you can follow these steps to make sure your config is correct and working: + +1. Clone our repo for push testing git clone git@github.com:GetStream/chat-push-test.git + +2. `cd flutter` + +3. In folder run `flutter pub get` + +4. Input your api key and secret in `lib/main.dart` + +5. Change the bundle identifier/application ID and development team/user so you can run the app in your device (**do not** run on iOS simulator, Android emulator is fine) + +6. Add your `google-services.json/GoogleService-Info.plist` + +7. Run the app + +8. Accept push notification permission (iOS only) + +9. Tap on `Device ID` and copy it + +10. Send the app to background + +11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID + +```shell +stream chat:push:test -u +``` + +You should get a test push notification + +### App in the background but still connected + +The [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html) widget lets you define a [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) handler in order to handle events while the app is in the background, but the client is still connected. + +This is useful because it lets you keep the connection alive in cases in which the app goes in the background just for some seconds (eg: multitasking, picking pictures from the gallery...) + +You can even customize the [backgroundKeepAlive](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/backgroundKeepAlive.html) duration. + +In order to show notifications in such a case we suggest using the package [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications); follow the package guide to successfully set up the plugin. + +Once that's done you should set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html); here is an example: + +```dart +... +StreamChat( + client: client, + onBackgroundEventReceived: (e) { + final currentUserId = client.state.user.id; + if (![ + EventType.messageNew, + EventType.notificationMessageNew, + ].contains(event.type) || + event.user.id == currentUserId) { + return; + } + if (event.message == null) return; + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final initializationSettingsAndroid = + AndroidInitializationSettings('launch_background'); + final initializationSettingsIOS = IOSInitializationSettings(); + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.show( + event.message.id.hashCode, + event.message.user.name, + event.message.text, + NotificationDetails( + android: AndroidNotificationDetails( + 'message channel', + 'Message channel', + 'Channel used for showing messages', + priority: Priority.high, + importance: Importance.high, + ), + iOS: IOSNotificationDetails(), + ), + ); + }, + child: .... +); +... +``` + +As you can see we generate a local notification whenever a message.new or notification.message_new event is received. + +:::note +Using `flutter_local_notifications` is a great way to implement notifications while the is in foreground too! You can generate a local notification listening to events using the method `streamChatClient.on()` and react to the events you want. +::: + +### Saving notification messages to the offline storage + +You may want to save received messages when you receive them via a notification so that later on when you open the app they're already there. + +To do this we need to update the push notification data payload at Stream Dashboard and clear the notification one: + +```json +{ + "message_id": "{{ message.id }}", + "channel_id": "{{ channel.id }}", + "channel_type": "{{ channel.type }}" +} +``` + +Then we need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, learn [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up. + +Then during the call `firebaseMessaging.configure(...)` we need to set the `onBackgroundMessage` parameter using a TOP-LEVEL or STATIC function to handle background messages; here is an example: + +```dart +Future myBackgroundMessageHandler(message) async { + if (message.containsKey('data')) { + final data = message['data']; + final messageId = data['message_id']; + final channelId = data['channel_id']; + final channelType = data['channel_type']; + final cid = '$channelType:$channelId'; + + final client = StreamChatClient(apiKey); + final persistenceClient = StreamChatPersistenceClient(); + await persistenceClient.connect(userId); + + final message = await client.getMessage(messageId).then((res) => res.message); + + await persistenceClient.updateMessages(cid, [message]); + persistenceClient.disconnect(); + + /// This can be done using the package flutter_local_notifications as we did before 👆 + _showLocalNotification(); + } +} +``` diff --git a/docusaurus/docs/Flutter/guides/introduction.mdx b/docusaurus/docs/Flutter/guides/introduction.mdx new file mode 100644 index 00000000..fc8f385e --- /dev/null +++ b/docusaurus/docs/Flutter/guides/introduction.mdx @@ -0,0 +1,5 @@ +--- +id: introduction +sidebar_position: 1 +title: Introduction +--- \ No newline at end of file diff --git a/docusaurus/docs/Flutter/guides/migration_guide_2_0.mdx b/docusaurus/docs/Flutter/guides/migration_guide_2_0.mdx new file mode 100644 index 00000000..394137bd --- /dev/null +++ b/docusaurus/docs/Flutter/guides/migration_guide_2_0.mdx @@ -0,0 +1,298 @@ +--- +id: mig_guide_2_0 +sidebar_position: 5 +title: Migrating to 2.0 (Null-safety) +--- + +A Migration Guide For Switching To v2.0 Of The Flutter SDK + +### Overview + +v2.0 of the Stream Chat Flutter SDK brings along several changes - primarily making the SDK null-safe. +Null safety allows your apps to run faster, with fewer errors, and with less code. + +Check [this link](https://flutter.dev/docs/null-safety) for more about Null Safety in Flutter. + +This guide is intended to enumerate and better explain the changes in the SDK. + +The changes will be listed by package and a concise changelog will follow with more info. + +### Changelog of `stream_chat_flutter` + +#### 🛑️ Breaking Changes from 1.5.4 + +* Migrate this package to null safety + +* Renamed `ChannelImage` to `ChannelAvatar` + +* Updated `StreamChatThemeData.reactionIcons` to accept custom builder + +* Renamed `ColorTheme` properties to reflect the purpose of the colors + + * `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + * `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + * `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + * `ColorTheme.greyWhisper` -> `ColorTheme.borders` + * `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + * `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + * `ColorTheme.white` -> `ColorTheme.barsBg` + * `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + * `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + * `ColorTheme.accentRed` -> `ColorTheme.accentError` + * `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + +* `ChannelListCore` options property is removed in favor of individual properties + + * `options.state` -> `bool state` + * `options.watch` -> `bool watch` + * `options.presence` -> `bool presence` + +* `UserListView` options property is removed in favor of individual properties + + * `options.presence` -> `bool presence` + +* Renamed `ImageHeader` to `GalleryHeader` + +* Renamed `ImageFooter` to `GalleryFooter` + +* `MessageBuilder` and `ParentMessageBuilder` signature is now + +``` +typedef MessageBuilder = Widget Function( + BuildContext, + MessageDetails, + List, + MessageWidget defaultMessageWidget, + ); +``` + +The last parameter is the default `MessageWidget` +You can call `.copyWith` to customize just a subset of properties + +#### ✅ Added + +Added video compress options (frame and quality) to MessageInput +`TypingIndicator` now has a property called `parentId` to show typing indicator specific to threads +#493: add support for `MessageListView` header/footer +`MessageWidget` accepts a `userAvatarBuilder` +Added `pinMessage` ui support +Added `MessageListView.threadSeparatorBuilder` property +Added `MessageInput.onError` property to allow error handling +Added `GalleryHeader`/`GalleryFooter` theme classes + +#### 🐞 Fixed + +#483: Keyboard covers input text box when editing message +`Modals` are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +#484: messages don't update without a reload +`MessageListView` not rendering if the user is not a member of the channel +Fix `MessageInput` overflow when there are no actions +Minor fixes and improvements + +### Migrating to 2.0 for `stream_chat_flutter` + +:::note +If you are migrating your full Flutter project to null-safety, first make sure you follow the +instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide). +::: + +To migrate to v2.0 for `stream_chat_flutter`, first change the version of the package to the latest +null-safe version. + +```yaml +dependencies: + stream_chat_flutter: ^2.0.0 +``` + +Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to +remedy the issues: + +1) Replace the offending class names with the revised class names + + * `ChannelImage` -> `ChannelAvatar` + * `ImageHeader` -> `GalleryHeader` + * `ImageFooter` -> `GalleryFooter` + +2) The new version comes with revised color names since the previous names do not suit light/dark mode +nomenclature. Make sure any old colors used from theme are changed over to the new theme color names: + + * `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + * `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + * `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + * `ColorTheme.greyWhisper` -> `ColorTheme.borders` + * `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + * `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + * `ColorTheme.white` -> `ColorTheme.barsBg` + * `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + * `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + * `ColorTheme.accentRed` -> `ColorTheme.accentError` + * `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + +3) We decided to make messages easier to customize and now supply the default implementation of the +messages in the builder - so you can now customize a single parameter without having to redo the +entire implementation. Please reform your builders to take into account the new format: + +``` +typedef MessageBuilder = Widget Function( + BuildContext, + MessageDetails, + List, + MessageWidget defaultMessageWidget, + ); +``` + +To tweak any of the default properties individually, you can use `defaultMessageWidget.copyWith()`. + +### Changelog of `stream_chat_flutter_core` + +#### 🛑️ Breaking Changes from 1.5.3 + +* Migrate this package to null safety +* `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties + * `options.state` -> `bool state` + * `options.watch` -> `bool watch` + * `options.presence` -> `bool presence` +* `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties + * `options.presence` -> `bool presence` + +#### ✅ Added + +* Monitor connection using `connectivity_plus` package + +#### 🐞 Fixed + +* Minor fixes +* Performance improvements + +### Migrating to 2.0 for `stream_chat_flutter_core` + +:::note +If you are migrating your full Flutter project to null-safety, first make sure you follow the +instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide). +::: + +To migrate to v2.0 for `stream_chat_flutter_core`, first change the version of the package to the latest +null-safe version. + +```yaml +dependencies: + stream_chat_flutter_core: ^2.0.0 +``` + +Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to +remedy the issue: + +:::note +The major changes in `stream_chat_flutter_core` consist of changing over from a map full of options +to a more type safe and sound approach by changing over to explicit parameters. +::: + +1) Change over Core widget implementations by using the explicit parameters instead of the options map. +Use these explicit parameters in the widget constructor instead of the option keys: + + * `options.state` -> `bool state` + * `options.watch` -> `bool watch` + * `options.presence` -> `bool presence` + +2) Change over query calls in the BLoCs in the same way (change from options map to explicit parameters +in the constructor) + +### Changelog of `stream_chat` + +#### 🛑️ Breaking Changes from 1.5.3 + +* Migrate this package to null safety +* `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor) +* `client.disconnect()` is now divided into two different functions + * `client.closeConnection()` -> for closing user websocket connection. + * `client.disconnectUser()` -> for disconnecting user and resetting client state. +* `client.devToken()` now returns a Token model instead of String. +* `ApiError` is removed in favor of `StreamChatError` + * `StreamChatError` -> parent type for all the stream errors. + * `StreamWebSocketError` -> for user websocket related errors. + * `StreamChatNetworkError` -> for network related errors. +* `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params + * `option.state` -> `bool state` + * `option.watch` -> `bool watch` + * `option.presence` -> `bool presence` +* `client.queryUsers()` options param is removed in favor of individual params + * `option.presence` -> `bool presence` +* Added typed filters + +#### 🐞 Fixed + +* #369: Client does not return without internet connection +* Several minor fixes +* Performance improvements + +#### ✅ Added + +* New Location enum is introduced for easily changing the client location/baseUrl. +* New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection. +* New `client.partialUpdateMessage` and `channel.partialUpdateMessage` methods +* `connectWebSocket` parameter in connect user calls to use the client in "connection-less" mode. + +#### 🔄 Changed + +* `baseURL` is now deprecated in favor of using Location to change data location. + +### Migrating to 2.0 for `stream_chat` + +If you are migrating your full Flutter project to null-safety, first make sure you follow the +instructions from the [official Null Safety migration guide](https://dart.dev/null-safety/migration-guide). +::: + +To migrate to v2.0 for `stream_chat`, first change the version of the package to the latest +null-safe version. + +```yaml +dependencies: + stream_chat: ^2.0.0 +``` + +Upon doing this, all breaking changes from the package will take immediate effect. Here are steps to +remedy the issues: + +1) Change over the constructor of `connectUserWithProvider()` to the new format which has `tokenProvider` as a required param. + +2) We added more nuance to `disconnectUser()` by adding two new methods - one to close the connection +and the other to disconnect the user. This allows more fine-grained control of disconnection. + + * `client.closeConnection()` -> for closing user websocket connection. + * `client.disconnectUser()` -> for disconnecting user and resetting client state. + +3) We refactored how we handle errors - new error types are now introduced that replace ApiError. + + * `StreamChatError` -> parent type for all the stream errors. + * `StreamWebSocketError` -> for user websocket related errors. + * `StreamChatNetworkError` -> for network related errors. + +4) We changed over from a map full of options to a more type-safe and sound approach by changing over to explicit parameters. + +Use these explicit parameters in the query parameters instead of the option keys: + +* `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params + * `option.state` -> `bool state` + * `option.watch` -> `bool watch` + * `option.presence` -> `bool presence` +* `client.queryUsers()` options param is removed in favor of individual params + * `option.presence` -> `bool presence` + +5) We added type-safe filters to make filtering in the app easier. Change over the filters to the +new implementation. + +As an example, in the old app this filter: + +```dart + filter: { + 'members': { + '\$in': [StreamChat.of(context).user.id], + } + }, +``` + +Would turn into: + +```dart + filter: Filter.in_('members', [StreamChat.of(context).user.id]) +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/_category_.json b/docusaurus/docs/Flutter/stream_chat_flutter/_category_.json new file mode 100644 index 00000000..242afb7b --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Stream Chat Flutter", + "position": 3 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_header.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/channel_header.mdx new file mode 100644 index 00000000..bf65a863 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/channel_header.mdx @@ -0,0 +1,84 @@ +--- +id: channel_header +sidebar_position: 10 +title: ChannelHeader +--- + +A Widget To Display Common Channel Details + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelHeader-class.html) + +![](../assets/channel_header.png) + +### Background + +When a user opens a channel, it is helpful to provide context of which channel they are in. This may +be in the form of a channel name or the users in the channel. Along with that, there also needs to be +a way for the user to look at more details of the channel (media, pinned messages, actions, etc.) and +preferably also a way to navigate back to where they came from. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `ChannelHeader` +widget which provides these out of the box. + +### Basic Example + +Let's just add a `ChannelHeader` to a page with a `MessageListView` and a `MessageInput` to display +and send messages. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + ), + ), + MessageInput(), + ], + ), + ); + } +} +``` + +### Customizing Parts Of The Header + +The header works like a `ListTile` widget. + +Use the `title`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own. + +```dart +//... +ChannelHeader( + title: Text('My Custom Name'), +), +``` + +![](../assets/channel_header_custom_title.png) + +### Showing Connection State + +The `ChannelHeader` can also display connection state below the tile which shows the user if they +are connected or offline, etc. on connection events. + +To enable this, use the `showConnectionStateTile` property. + +```dart +//... +ChannelHeader( + showConnectionStateTile: true, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_header.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_header.mdx new file mode 100644 index 00000000..d6930cb4 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_header.mdx @@ -0,0 +1,88 @@ +--- +id: channel_list_header +sidebar_position: 9 +title: ChannelListHeader +--- + +A Header Widget For A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListHeader-class.html) + +![](../assets/channel_list_header.png) + +### Background + +A common pattern for most messaging apps is to show a list of Channels (chats) on the first screen +and navigate to an individual one on being clicked. On this first page where the list of channels are +displayed, it is usual to have functionality such as adding a new chat, display the user logged in, etc. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `ChannelListHeader` +widget which provides these out of the box. + +### Basic Example + +This is a basic example of a page which has a `ChannelListView` and a `ChannelListHeader` to recreate a +common Channels Page. + +```dart +class DemoPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ChannelListHeader(), + body: ChannelsBloc( + child: ChannelListView( + filter: Filter.in_('members', [StreamChat.of(context).user.id]), + sort: [SortOption('last_message_at')], + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ), + ); + } +} +``` + +### Customizing Parts Of The Header + +The header works like a `ListTile` widget. + +Use the `titleBuilder`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own. + +```dart +//... +ChannelListHeader( + subtitle: Text('My Custom Subtitle'), +), +``` + +![](../assets/channel_list_header_custom_subtitle.png) + +The `titleBuilder` param helps you build different titles depending on the connection state: + +```dart +//... +ChannelListHeader( + titleBuilder: (context, status, client) { + switch(status) { + /// Return your title widget + } + }, +), +``` + +### Showing Connection State + +The `ChannelListHeader` can also display connection state below the tile which shows the user if they +are connected or offline, etc. on connection events. + +To enable this, use the `showConnectionStateTile` property. + +```dart +//... +ChannelListHeader( + showConnectionStateTile: true, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_view.mdx new file mode 100644 index 00000000..eb7baf20 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_view.mdx @@ -0,0 +1,111 @@ +--- +id: channel_list_view +sidebar_position: 4 +title: ChannelListView +--- + +A Widget For Displaying A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListView-class.html) + +![](../assets/channel_list_view.png) + +### Background + +Channels are fundamental elements of Stream Chat and constitute shared spaces which allow users to +message each other. + +1:1 conversations and groups are both examples of channels, albeit with some (distinct/non-distinct) +differences. Displaying the list of channels that a user is a part of is a pattern present in most messaging apps. + +The `ChannelListView` widget allows displaying a list of channels to a user. By default, this is NOT +ONLY the channels that the user is a part of. This section goes into setting up and using a `ChannelListView` +widget. + +### Basic Example + +Here is a basic example of the `ChannelListView` widget. It consists of the main widget itself, a `Filter` +to filter only the channels that the user is a part of, sorting by last message time, pagination params, +and the widget to use when a particular channel is clicked. + +```dart +class ChannelListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: ChannelsBloc( + child: ChannelListView( + filter: Filter.in_('members', [StreamChat.of(context).user.id]), + sort: [SortOption('last_message_at')], + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ), + ); + } +} +``` + +This example by default displays the channels that a user is a part of. Now let's look at customizing +the widget. + +### Customizing the Channel Preview + +A common aspect of the widget needed to be tweaked according to each app is the Channel Preview (the +Channel tile in the list). To do this, we use the `channelPreviewBuilder` param like this: + +```dart +ChannelListView( + ... + channelPreviewBuilder: (context, channel) { + return ListTile( + tileColor: Colors.amberAccent, + title: Center( + child: ChannelName(), + ), + ); + }, +), +``` + +Which gives you a new Channel preview in the list: + +![](../assets/channel_preview.png) + +### Adding Swipe Actions + +To add actions (such as delete, more info, etc) when Channel preview is swiped left, set the `swipeToAction` +parameter to `true`. + +```dart +ChannelListView( + ... + swipeToAction: true, +), +``` + +This adds two basic actions - info and delete: + +![](../assets/swipe_channel.png) + +To add custom actions of your own, use the `swipeActions` param: + +```dart +ChannelListView( + ... + swipeToAction: true, + swipeActions: [ + SwipeAction( + color: Colors.blue, + iconWidget: Icon(Icons.add), + onTap: (channel) { + // Things to do on icon tap + }, + ), + // Other actions here + ] +), +``` + diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/introduction.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/introduction.mdx new file mode 100644 index 00000000..28640483 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/introduction.mdx @@ -0,0 +1,18 @@ +--- +id: introduction +sidebar_position: 1 +title: Introduction +--- + +Understanding The UI Package Of The Flutter SDK + +### What function does `stream_chat_flutter` serve? + +The UI SDK (`stream_chat_flutter`) contains official Flutter components for Stream Chat, a service for building chat applications. + +While the Stream Chat service provides the backend for messaging and the LLC provides an easy way to +use it in your Flutter apps, we wanted to make sure that adding Chat functionality to your app was as quick as possible. + +The UI package is built on top of the low-level client and the core package and allows you to build a +full fledged app with either the inbuilt components, modify existing components, or easily add widgets +of your own to match your app's style better. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_input.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/message_input.mdx new file mode 100644 index 00000000..f33f85e9 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/message_input.mdx @@ -0,0 +1,170 @@ +--- +id: message_input +sidebar_position: 6 +title: MessageInput +--- + +A Widget Dealing With Everything Related To Sending A Message + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageInput-class.html) + +![](../assets/message_input.png) + +### Background + +In Stream Chat, we can send messages in a channel. However, sending a message isn't as simple as adding +a `TextField` and logic for sending a message. It involves additional processes like addition of media, +quoting a message, adding a custom command like a GIF board, and much more. Moreover, most apps also +need to customize the input to match their theme, overall color and structure pattern, etc. + +To do this, we created a `MessageInput` widget which abstracts all expected functionality a modern input +needs - and allows you to use it out of the box. + +### Basic Example + +A `StreamChannel` is required above the widget tree in which the `MessageInput` is rendered since the channel is +where the messages sent actually go. Let's look at a common example of how we could use the `MessageInput`: + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + ), + ), + MessageInput(), + ], + ), + ); + } +} +``` + +It is common to put this widget in the same page of a `MessageListView` as the bottom widget. + +### Quoting A Message + +The quoting functionality allows us to 'reply' to a specific message without creating a thread out of it. +It adds the other message as context when sending a message and also displays it above the sent message. + +To quote a message, we provide a `quotedMessage` to the `MessageInput`. + +```dart +Message? message; + +// ... +MessageInput( + quotedMessage: message, +), +``` + +This will add the message given above the message about to be sent. + +While you can implement your own functionality to select which message to reply to, the `MessageListView` +widget helps in this case since it has an inbuilt `onMessageSwiped` callback which we can use. + +```dart + +class ChannelPage extends StatefulWidget { + @override + _ChannelPageState createState() => _ChannelPageState(); +} + +class _ChannelPageState extends State { + Message? quotedMessage; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MessageListView( + // ... + onMessageSwiped: (message) { + setState(() { + quotedMessage = message; + }); + }, + ), + ), + MessageInput( + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + }, + ), + ], + ), + ); + } +} +``` + +![](../assets/message_input_quoted_message.png) + +### Adding Custom Actions + +By default, the `MessageInput` has two actions: one for attachments and one for commands like Giphy. +To add your own action, we use the `actions` parameter like this: + +```dart +MessageInput( + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + // Do something here + }, + ), + ], +), +``` + +This will add on your action to the existing ones. + +### Disable Attachments + +To disable attachments being added to the message, set the `disableAttachments` parameter to true. + +```dart +MessageInput( + disableAttachments: true, +), +``` + +### Changing Position Of MessageInput Components + +You can also change the position of the TextField, actions and 'send' button relative to each other. + +To do this, use the `actionsLocation` or `sendButtonLocation` parameters which help you decide the location +of the buttons in the input. + +For example, if we want the actions on the right and the send button inside the TextField, we can do: + +```dart +MessageInput( + sendButtonLocation: SendButtonLocation.inside, + actionsLocation: ActionsLocation.right, +), +``` + +![](../assets/message_input_change_position.png) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/message_list_view.mdx new file mode 100644 index 00000000..0ac78f7d --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/message_list_view.mdx @@ -0,0 +1,115 @@ +--- +id: message_list_view +sidebar_position: 5 +title: MessageListView +--- + +A Widget For Displaying A List Of Messages + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageListView-class.html) + +![](../assets/message_list_view.png) + +### Background + +Every channel can contain a list of messages sent by users inside it. The `MessageListView` widget +displays the list of messages inside a particular channel along with possible attachments and +other message attributes (if the message is pinned for example). This sets it apart from the `MessageSearchListView` +which may not contain messages only from a single channel and is used to search for messages across +many. + +### Basic Example + +The `MessageListView` shows the list of messages of the current channel. It has inbuilt support for +common messaging functionality: displaying and editing messages, adding / modifying reactions, support +for quoting messages, pinning messages, and more. + +An example of how you can use the MessageListView is: + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + ), + ), + MessageInput(), + ], + ), + ); + } +} +``` + +### Enable Threads + +Threads are made of a parent message and replies linked to it. To enable threading, the SDK requires you +to supply a `threadBuilder` which will supply the page when the thread is clicked. + +```dart +MessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, +), +``` + +![](../assets/message_list_view_threads.png) + +The `MessageListView` itself can render the thread by supplying the `parentMessage` parameter. + +```dart +MessageListView( + parentMessage: parent, +), +``` + +### Building Custom Messages + +You can also supply your own implementation for displaying messages using the `messageBuilder` parameter. + +:::note +To customize the existing implementation, look at the `MessageWidget` documentation instead. +::: + +```dart +MessageListView( + messageBuilder: (context, details, messageList, defaultImpl) { + // Your implementation of the message here + // E.g: return Text(details.message.text ?? ''); + }, +), +``` + +### Enabling Message Pinning + +Message pins save and highlight the message in the `MessageListView`. To enable users to pin the message, +make sure the pin permissions are granted for different types of users on the dashboard. After confirming +the appropriate users have permissions, add the user types in the `pinPermissions` parameter. + +```dart +MessageListView( + //... + pinPermissions: ['admin', 'userType1', 'userType2'], +), +``` + +This will allow these user types to pin messages through the message actions modal. + +![](../assets/message_list_view_pin.png) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx new file mode 100644 index 00000000..864b5e49 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx @@ -0,0 +1,62 @@ +--- +id: message_search_list_view +sidebar_position: 8 +title: MessageSearchListView +--- + +A Widget To Search For Messages Across Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageSearchListView-class.html) + +![](../assets/message_search_list_view.png) + +### Background + +Users in Stream Chat can have several channels and it can get hard to remember which channel has the +message they are searching for. As such, there needs to be a way to search for a message across multiple +channels. This is where `MessageSearchListView` comes in. + +### Basic Example + +While the MessageListView is tied to a certain `StreamChannel`, a `MessageSearchListView` is not. + +```dart +class MessageSearchPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: MessageSearchBloc( + child: MessageSearchListView( + filters: Filter.in_('members', [StreamChat.of(context).user!.id],), + messageQuery: 'your query here', + paginationParams: PaginationParams(limit: 20), + ), + ), + ); + } +} +``` + +### Customize The Result Tiles + +You can use your own widget for the result items using the `itemBuilder` parameter. + +```dart +MessageSearchListView( + // ... + itemBuilder: (context, response) { + return Text(response.message.text); + }, +), +``` + +### Show Result Count + +You show the number of results via the `showResultCount` parameter. + +```dart +MessageSearchListView( + // ... + showResultCount: true, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_widget.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/message_widget.mdx new file mode 100644 index 00000000..5060bcac --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/message_widget.mdx @@ -0,0 +1,98 @@ +--- +id: message_widget +sidebar_position: 11 +title: MessageWidget +--- + +A Widget For Displaying Messages And Attachments + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageWidget-class.html) + +### Background + +There are several things that need to be displayed with text in a message in a modern messaging app: +attachments, highlights if the message is pinned, user avatars of the sender, etc. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `MessageWidget` +widget which provides these out of the box. + +### Basic Example (Modifying `MessageWidget` in `MessageListView`) + +Primarily, the `MessageWidget` is used in the `MessageListView`. To customize only a few properties +of the `MessageWidget` without supplying all other properties, the `messageBuilder` builder supplies +a default implementation of the widget for us to modify. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MessageListView( + messageBuilder: (context, details, messageList, defaultMessageWidget) { + return defaultMessageWidget.copyWith( + showThreadReplyIndicator: false, + ); + }, + ), + + ); + } +} +``` + +### Building A Custom Attachment + +When a custom attachment type (location, audio, etc.) is sent, the MessageWidget also needs to know +how to build it. For this purpose, we can use the `customAttachmentBuilders` parameter. + +As an example, if a message has a attachment type 'location', we do: + +```dart +MessageWidget( + //... + customAttachmentBuilders: { + 'location': (context, message, attachments) { + var attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, +) +``` + +You can also override the builder for existing attachment types like `image` and `video`. + +### Show User Avatar For Messages + +You can decide to show, hide, or remove user avatars of the sender of the message. To do this, set +the `showUserAvatar` property like this: + +```dart +MessageWidget( + //... + showUserAvatar = DisplayWidget.show, +) +``` + +### Reverse the message + +In most cases, `MessageWidget` needs to be a different orientation depending upon if the sender is the +user or someone else. + +For this, we use the `reverse` parameter to change the orientation of the message: + +```dart +MessageWidget( + //... + reverse = true, +) +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/setup.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/setup.mdx new file mode 100644 index 00000000..22e5c6c5 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/setup.mdx @@ -0,0 +1,48 @@ +--- +id: setup +sidebar_position: 2 +title: Setup +--- + +Understanding Setup For `stream_chat_flutter` + +### Add pub.dev dependency + +First, you need to add the `stream_chat_flutter` dependency to your `pubspec.yaml`. + +You can either run this command: + +```shell +flutter pub add stream_chat_flutter +``` + +OR + +Add this line in the dependencies section of your pubspec.yaml after substituting latest version: + +```yaml +dependencies: + stream_chat_flutter: ^latest_version +``` + +You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter). + +### Details On Platform Support + +`stream_chat_flutter` was originally created for Android and iOS mobile platforms. As Flutter matured, +support for additional platforms was added and the package now has experimental support for web and desktop as +[detailed here](https://getstream.io/blog/announcing-experimental-multi-platform-support-for-the-stream-flutter-sdk/). + +However, platforms other than mobile may have additional constraints due to not supporting all plugins, +which will be addressed by the respective plugin creators over time. + +### Setup: iOS + +The library uses [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) to pick files from the os. +Follow [this wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/Setup#ios) to fulfill iOS requirements. + +We also use [video_player](https://pub.dev/packages/video_player) to reproduce videos. +Follow [this guide](https://pub.dev/packages/video_player#installation) to fulfill the requirements. + +To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin. +Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx new file mode 100644 index 00000000..d527b738 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx @@ -0,0 +1,71 @@ +--- +id: stream_chat_and_theming +sidebar_position: 3 +title: StreamChat And Theming +--- + +Understanding How To Customize Widgets Using `StreamChatTheme` + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatTheme-class.html) and [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatThemeData-class.html) + +### Background + +Stream's UI SDK makes it easy for developers to add custom styles and attributes to our widgets. Like most Flutter frameworks, Stream exposes a dedicated widget for theming. + +Using `StreamChatTheme`, users can customize most aspects of our UI widgets by setting attributes using `StreamChatThemeData`. + +Similar to the `Theme` and `ThemeData` in Flutter, Stream Chat uses a top level [inherited widget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) to provide theming information throughout your application. This can be optionally set at the top of your application tree or at a localized point in your widget sub-tree. + +If you'd like to customize the look and feel of Stream chat across your entire application, we recommend setting your theme at the top level. Conversely, users can customize specific screens or widgets by wrapping components in a `StreamChatTheme`. + +### A closer look at StreamChatThemeData + +Looking at the constructor for `StreamChatThemeData`, we can see the full list of properties and widgets available for customization. + +Some high-level properties such as `textTheme` or `colorTheme` can be set application-wide directly from this class. In contrast, larger components such as `ChannelHeader`, `MessageInputs`, etc. have been broken up into smaller theme objects. + +```dart +factory StreamChatThemeData({ + Brightness? brightness, + TextTheme? textTheme, + ColorTheme? colorTheme, + ChannelListHeaderTheme? channelListHeaderTheme, + ChannelPreviewTheme? channelPreviewTheme, + ChannelTheme? channelTheme, + MessageTheme? otherMessageTheme, + MessageTheme? ownMessageTheme, + MessageInputTheme? messageInputTheme, + Widget Function(BuildContext, Channel)? defaultChannelImage, + Widget Function(BuildContext, User)? defaultUserImage, + IconThemeData? primaryIconTheme, + List? reactionIcons, + }); +``` + +### Stream Chat Theme in use + +Let's take a look at customizing widgets using `StreamChatTheme`. In the example below, we can change the default color theme to yellow and override the channel header's typography and colors. + +```dart +builder: (context, child) => StreamChat( + client: client, + child: child, + streamChatThemeData: StreamChatThemeData( + colorTheme: ColorTheme.light( + primaryAccent: const Color(0xffffe072), + ), + channelTheme: ChannelTheme( + channelHeaderTheme: ChannelHeaderTheme( + color: const Color(0xffd34646), + title: TextStyle( + color: Colors.white, + ), + ), + ), + ), + ), +``` + +We are creating this class at the very top of our widget tree using the `streamChatThemeData` parameter found in the `StreamChat` widget. + +![](../assets/using_theme.jpg) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/user_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/user_list_view.mdx new file mode 100644 index 00000000..61dfbeb4 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/user_list_view.mdx @@ -0,0 +1,88 @@ +--- +id: user_list_view +sidebar_position: 7 +title: UserListView +--- + +A Widget For Displaying And Selecting Users + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/UserListView-class.html) + +![](../assets/user_list_view.png) + +### Background + +A list of users is required for many different purposes: showing a list of users in a Channel, +selecting users to add in a channel, etc. The `UserListView` displays and allows selection of a list +of users along with multiple display configurations like a list and grid. + +### Basic Example + +Let's take a look at an example where we use the widget to autocomplete user names: + +```dart +class UsersListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: UsersBloc( + child: UsersListView( + filter: Filter.notEqual('id', StreamChat.of(context).user!.id), + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + pagination: PaginationParams( + limit: 25, + ), + ), + ), + ); + } +} +``` + +### Customize The User Items + +You can use your own widget for the user items using the `userItemBuilder` parameter. + +```dart +UsersListView( + // ... + userItemBuilder: (context, user, isSelected) { + return Text(user.name); + }, +), +``` + +### Group Alphabetically + +You can group alphabetically using the `groupAlphabetically` parameter: + +```dart +UsersListView( + //... + groupAlphabetically: true, +), +``` + +### Selecting Users + +The `UserListView` widget allows selecting users in a list by supplying a selected users list and callbacks +for when user items are tapped. + +```dart +Set? selectedUsers = {}; + +UsersListView( + //... + selectedUsers: selectedUsers, + onUserTap: (user, _) { + setState(() { + selectedUsers.add(user); + }); + }, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/_category_.json b/docusaurus/docs/Flutter/stream_chat_flutter_core/_category_.json new file mode 100644 index 00000000..8d738e89 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Stream Chat Flutter Core", + "position": 4 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/channel_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/channel_list_core.mdx new file mode 100644 index 00000000..4196a14c --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/channel_list_core.mdx @@ -0,0 +1,68 @@ +--- +id: channel_list_core +sidebar_position: 4 +title: ChannelListCore +--- + +A Widget For Building A List Of Channels + +### Background + +The UI SDK of Stream Chat supplies a `ChannelListView` class that builds a list of channels fetching +according to the filters and sort order given. However, in some cases, implementing novel UI is necessary +that cannot be done using the customization approaches given in the widget. + +To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that +fetches channels in the expected way via the usual params but does not supply any UI and instead +exposes builders to build the UI in situations such as loading, empty data, errors, and on data received. + +### Basic Example + +`ChannelListCore` is a simplified class that allows fetching a list of +channels while exposing UI builders. + +This allows you to construct your own UI while not having to +worry about the specific logic of fetching channels in your app. + +A `ChannelListController` is used to reload and paginate data. + +```dart +class ChannelListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: ChannelListCore( + filter: Filter.in_( + 'members', + [StreamChat.of(context).user!.id], + ), + sort: [SortOption('last_message_at')], + pagination: PaginationParams( + limit: 20, + ), + errorBuilder: (context, err) { + return Center( + child: Text('An error has occured'), + ); + }, + emptyBuilder: (context) { + return Center( + child: Text('Nothing here...'), + ); + }, + loadingBuilder: (context) { + return Center( + child: CircularProgressIndicator(), + ); + }, + listBuilder: (context, list) { + return ChannelPage(list); + } + ), + ); + } +} +``` + +Make sure to have a `StreamChatCore` ancestor in order to provide the +information about the channels. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/channels_bloc.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/channels_bloc.mdx new file mode 100644 index 00000000..3cb1728a --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/channels_bloc.mdx @@ -0,0 +1,90 @@ +--- +id: channels_bloc +sidebar_position: 7 +title: ChannelsBloc +--- + +A Widget Dedicated To The Management Of A Channel List With Pagination. + +### Background + +Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels, +messages, users, etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current +data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or +existing data and build UI based on it. + +All Core and UI widgets which focus on fetching a list of objects need to have their respective functions +above them in the widget tree. The ChannelListCore and ChannelListView require the ChannelsBloc +above them in the widget hierarchy without which they will fail. + +### Understanding The Widget + +`ChannelsBloc` is used together with `ChannelListCore` to manage a list of +Channels with pagination, re-ordering, querying and other operations +associated with Channels. + +`ChannelsBloc` can be accessed at anytime by using the static `.of` method +using Flutter's `BuildContext`. + +```dart +var _channelsBloc = ChannelsBloc.of(context); +``` + +The `ChannelsBloc` widget encapsulates common functionality related to channel lists such as fetching +the existing channels and querying new channels and also supplies them down the widget tree. + +The widget is required for the respective core widget (`ChannelListCore`) to fetch channels and hence +must be above the core widget in the tree. + +Here is a basic implementation of `ChannelsBloc`: + +```dart +ChannelsBloc( + child: // Further Widget Tree +), +``` + +The `ChannelsBloc` widget allows three customisations: + +#### Lock Channels Order + +ChannelsBloc may change the order of channels when new messages arrive. To lock this order, we can +set the `lockChannelsOrder` property to true. + +```dart +ChannelsBloc( + lockChannelsOrder: true, + child: // Further Widget Tree +), +``` + +#### Set custom channel order + +We can decide the order of the channels in the list by supplying a comparator to the `channelsComparator` +parameter: + +```dart +ChannelsBloc( + channelsComparator: (a, b) { + return a.createdAt!.millisecondsSinceEpoch > + b.createdAt!.millisecondsSinceEpoch + ? 1 + : -1; + }, + child: // Further Widget Tree +), +``` + +#### Decide if channel should be added on new message event + +When a new message arrives, a `message.new` event is received. We can decide if we want to add the channel +to the list using the `shouldAddChannel` parameter which is a callback supplying the event data: + +```dart +ChannelsBloc( + shouldAddChannel: (event) { + return event.message!.extraData['priority'] == '1'; + }, + child: // Further Widget Tree +), +``` \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx new file mode 100644 index 00000000..3c6a70f2 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx @@ -0,0 +1,80 @@ +--- +id: introduction +sidebar_position: 1 +title: Introduction +--- + +Understanding The Core Package Of The Flutter SDK + +This package provides business logic to fetch common things required for integrating Stream Chat into your application. +The core package allows more customisation and hence provides business logic but no UI components. +Please use the `stream_chat_flutter` package for the full fledged suite of UI components or `stream_chat` for the low-level client. + +### Background + +In the early days of the Flutter SDK, the SDK was only split into the LLC (`stream_chat`) and +the UI package (`stream_chat_flutter`). With this you could use a fully built interface with the UI package +or a fully custom interface with the LLC. However, we soon recognised the need for a third intermediary +package which made tasks like building and modifying a list of channels or messages easy but without +the complexity of using low level components. The Core package (`stream_chat_flutter_core`) is a manifestation +of the same idea and allows you to build an interface with Stream Chat without having to deal with +low level code and architecture as well as implementing your own theme and UI effortlessly. +Also, it has very few dependencies. + +We will now explore the components of this intermediary package and understand how it helps you build +the experience you want your users to have. + +The package primarily contains three types of classes: + +* Business Logic Components +* Core Components +* Core Controllers + +### Business Logic Components + +These components allow you to have the maximum and lower-level control of the queries being executed. + +In BLoCs, the basic functionalities - such as queries for messages, channels or queries - are bundled up +and passed along down the tree. Using a BLoC allows you to either create your own way to fetch and +build UIs or use an inbuilt Core widget to do the work such as queries, pagination, etc for you. + +The BLoCs we provide are: + +* ChannelsBloc +* MessageSearchBloc +* UsersBloc + +### Core Components + +Core components usually are an easy way to fetch data associated with Stream Chat. +Core components use functions exposed by the respective BLoCs (for example the ChannelListCore uses the ChannelsBloc) +and use the respective controllers for various operations. Unlike heavier components from the UI +package, core components are decoupled from UI and they expose builders instead to help you build +a fully custom interface. + +Data fetching can be controlled with the controllers of the respective core components. + +* ChannelListCore (Fetch a list of channels) +* MessageListCore (Fetch a list of messages from a channel) +* MessageSearchListCore (Fetch a list of search messages) +* UserListCore (Fetch a list of users) +* StreamChatCore (This is different from the other core components - it is a version of StreamChat decoupled from theme and initialisations.) + +### Core Controllers + +Core Controllers are supplied to respective CoreList widgets which allows reloading and pagination of data whenever needed. + +Unlike the UI package, the Core package allows a fully custom user interface built with the data. This +in turn provides a few challenges: we do not know implicitly when to paginate your list or reload your data. + +While this is handled out of the box in the UI package since the List implementation is inbuilt, a controller +needs to be used in the core package notifying the core components to reload or paginate the data existing +currently. For this, each core component has a respective controller which you can use to call the +specific function (reload / paginate) whenever such an event is triggered through / needed in your UI. + +* ChannelListController +* MessageListController +* MessageSearchListController +* ChannelListController + +This section goes into the individual core package widgets and their functional use. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx new file mode 100644 index 00000000..5fd5877a --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx @@ -0,0 +1,70 @@ +--- +id: message_list_core +sidebar_position: 5 +title: MessageListCore +--- + +A Widget For Building A List Of Messages + +### Background + +The UI SDK of Stream Chat supplies a `MessageListView` class that builds a list of channels fetching +according to the filters and sort order given. However, in some cases, implementing novel UI is necessary +that cannot be done using the customization approaches given in the widget. + +To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that +fetches channels in the expected way via the usual params but does not supply any UI and instead +exposes builders to build the UI in situations such as loading, empty data, errors, and on data received. + +### Basic Example + +`MessageListCore` is a simplified class that allows fetching a list of +messages while exposing UI builders. + +This allows you to construct your own UI while not having to +worry about the specific logic of fetching messages in a channel. + +A `MessageListController` is used to paginate data. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MessageListCore( + emptyBuilder: (context) { + return Center( + child: Text('Nothing here...'), + ); + }, + loadingBuilder: (context) { + return Center( + child: CircularProgressIndicator(), + ); + }, + messageListBuilder: (context, list) { + return MessagesPage(list); + }, + errorWidgetBuilder: (context, err) { + return Center( + child: Text('Error'), + ); + }, + ), + ), + ], + ), + ); + } +} +``` + +Make sure to have a `StreamChannel` ancestor in order to provide the +information about the channels. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_bloc.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_bloc.mdx new file mode 100644 index 00000000..15f5a6d6 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_bloc.mdx @@ -0,0 +1,40 @@ +--- +id: message_search_list_block +sidebar_position: 8 +title: MessageSearchListBloc +--- + +A Widget Used To Manage A List Of Messages With Pagination. + +### Background + +Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels, +messages, users etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current +data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or +existing data and build UI based on it. + +All Core and UI widgets which focus on fetching a list of objects need to have their respective functions +above them in the widget tree. The MessageSearchListCore and MessageSearchListView require the +MessageSearchListCore above them in the widget hierarchy without which they will fail. + +### Understanding The Widget + +This class can be used to load messages, perform queries, etc. + +`MessageSearchBloc` can be accessed at anytime by using the static `.of` method +using Flutter's BuildContext. + +```dart +var _searchBloc = MessageSearchBloc.of(context); +``` + +The `MessageSearchBloc` widget encapsulates common functionality related to searching for messages +across channels and also supplies them down the widget tree. + +Here is a basic implementation of `ChannelsBloc`: + +```dart +MessageSearchBloc( + child: // Further Widget Tree +), +``` \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx new file mode 100644 index 00000000..70cc026f --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx @@ -0,0 +1,41 @@ +--- +id: message_search_list_core +sidebar_position: 6 +title: MessageSearchListCore +--- + +A Widget For Displaying Message Searches + +### Background + +The UI SDK of Stream Chat supplies a `MessageSearchListView` class that builds a list of channels fetching +according to the filters and sort order given. However, in some cases, implementing novel UI is necessary +that cannot be done using the customization approaches given in the widget. + +To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that +fetches channels in the expected way via the usual params but does not supply any UI and instead +exposes builders to build the UI in situations such as loading, empty data, errors, and on data received. + +### Basic Example + +`MessageSearchListCore` is a simplified class that allows searching for + messages across channels while exposing UI builders. + A `MessageSearchListController` is used to load and paginate data. + +```dart +class MessageSearchPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: MessageSearchListCore( + messageQuery: _messageFilter, + filters: _channelsFilter, + paginationParams: PaginationParams(limit: 20), + ), + ); + } +} +``` + +Make sure to have a `MessageSearchBloc` ancestor in order to provide the +information about the messages. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/setup.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/setup.mdx new file mode 100644 index 00000000..15b15a56 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/setup.mdx @@ -0,0 +1,28 @@ +--- +id: setup +sidebar_position: 2 +title: Setup +--- + +Understanding Setup For `stream_chat_flutter_core` + +### Add pub.dev dependency + +First, you need to add the `stream_chat_flutter_core` dependency to your pubspec.yaml + +You can either run this command: + +```shell +flutter pub add stream_chat_flutter_core +``` + +OR + +Add this line in the dependencies section of your pubspec.yaml after substituting latest version: + +```yaml +dependencies: + stream_chat_flutter_core: ^latest_version +``` + +You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter_core). diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx new file mode 100644 index 00000000..2a43ae52 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx @@ -0,0 +1,26 @@ +--- +id: stream_chat_core +sidebar_position: 3 +title: StreamChatCore +--- + +`StreamChatCore` is a version of `StreamChat` found in `stream_chat_flutter` that is decoupled from +theme and initialisations. + +`StreamChatCore` is used to provide information about the chat client to the widget tree. +This Widget is used to react to life cycle changes and system updates. +When the app goes into the background, the websocket connection is automatically closed and when it goes back to foreground the connection is opened again. + +Like the `StreamChat` widget in the higher level UI package, the `StreamChatCore` widget should +be on the top level before using any Stream functionality: + +```dart +return MaterialApp( + title: 'Stream Chat Core Example', + home: HomeScreen(), + builder: (context, child) => StreamChatCore( + client: client, + child: child!, + ), + ); +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/user_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/user_list_core.mdx new file mode 100644 index 00000000..622d0025 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/user_list_core.mdx @@ -0,0 +1,64 @@ +--- +id: user_list_core +sidebar_position: 10 +title: UserListCore +--- + +A Widget For Building A List Of Users + +### Background + +The UI SDK of Stream Chat supplies a `UserListView` class that builds a list of channels fetching +according to the filters and sort order given. However, in some cases, implementing novel UI is necessary +that cannot be done using the customization approaches given in the widget. + +To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that +fetches channels in the expected way via the usual params but does not supply any UI and instead +exposes builders to build the UI in situations such as loading, empty data, errors, and on data received. + +### Basic Example + +`UserListCore` is a simplified class that allows fetching users while +exposing UI builders. +A `UserListController` is used to load and paginate data. + +```dart +class UsersListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: UsersListCore( + sort: [SortOption('last_active')], + pagination: PaginationParams( + limit: 20, + ), + errorBuilder: (err) { + return Center( + child: Text('An error has occured'), + ); + }, + emptyBuilder: (context) { + return Center( + child: Text('Nothing here...'), + ); + }, + emptyBuilder: (context) { + return Center( + child: CircularProgressIndicator(), + ); + }, + listBuilder: (context, list) { + return UsersPage(list); + } + ), + ); + } +} +``` + +`UsersBloc` must be the ancestor of this widget. This is necessary since +`UserListCore` depends on functionality contained within `UsersBloc`. + +The parameters `listBuilder`, `loadingBuilder`, `emptyBuilder` and +`errorBuilder` must all be supplied and not null. + diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/users_bloc.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/users_bloc.mdx new file mode 100644 index 00000000..61463599 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/users_bloc.mdx @@ -0,0 +1,37 @@ +--- +id: users_bloc +sidebar_position: 9 +title: UsersBloc +--- + +A Widget Dedicated To The Management Of A Users List With Pagination. + +### Background + +Most widgets in the Core SDK are focused on fetching a particular type of object from Stream Chat - channels, +messages, users, etc. The BLoC widgets bundle up the base functions used to fetch data as well as the current +data fetched by the respective functions. Furthermore, the Core widgets use this BLoC to fetch new or +existing data and build UI based on it. + +All Core and UI widgets which focus on fetching a list of objects need to have their respective functions +above them in the widget tree. The UserListCore and UserListView require the UserListCore +above them in the widget hierarchy without which they will fail. + +### Understanding The Widget + +`UsersBloc` can be accessed at anytime by using the static `.of` method +using Flutter's `BuildContext`. + +```dart +var _userBloc_ = UsersBloc.of(context); +``` + +The `UsersBloc` widget encapsulates common functionality related to user lists and also supplies them down the widget tree. + +Here is a basic implementation of `UsersBloc`: + +```dart +UsersBloc( + child: // Further Widget Tree +), +``` \ No newline at end of file diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index b82d3025..e05599e1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,49 @@ +## 2.0.0 + +🛑️ Breaking Changes from `1.5.3` + +- migrate this package to null safety +- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor) +- `client.disconnect()` is now divided into two different functions + - `client.closeConnection()` -> for closing user websocket connection. + - `client.disconnectUser()` -> for disconnecting user and resetting client state. +- `client.devToken()` now returns a `Token` model instead of `String`. +- `ApiError` is removed in favor of `StreamChatError` + - `StreamChatError` -> parent type for all the stream errors. + - `StreamWebSocketError` -> for user websocket related errors. + - `StreamChatNetworkError` -> for network related errors. +- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params + - `option.state` -> bool state + - `option.watch` -> bool watch + - `option.presence` -> bool presence +- `client.queryUsers()` options param is removed in favor of individual params + - `option.presence` -> bool presence +- Migrate this package to null safety +- Added typed filters + +🐞 Fixed + +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection +- several minor fixes +- performance improvements + + +✅ Added + +- New `Location` enum is introduced for easily changing the client location/baseUrl. +- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection. +- New `client.partialUpdateMessage` and `channel.partialUpdateMessage` methods +- `connectWebSocket` parameter in connect user calls to use the client in "connection-less" mode. + +🔄 Changed + +- `baseURL` is now deprecated in favor of using `Location` to change data location. + +## 2.0.0-nullsafety.8 + +🐞 Fixed +- Export `PushProvider` enum + ## 2.0.0-nullsafety.7 🛑️ Breaking Changes from `2.0.0-nullsafety.6` @@ -27,21 +73,10 @@ - `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 diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index bbcc86b8..167f5f6b 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -13,7 +13,7 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: { + extraData: const { 'image': 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', }, @@ -57,12 +57,10 @@ class StreamExample extends StatelessWidget { final Channel channel; @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Stream Chat Dart Example', - home: HomeScreen(channel: channel), - ); - } + Widget build(BuildContext context) => MaterialApp( + title: 'Stream Chat Dart Example', + home: HomeScreen(channel: channel), + ); } /// Main screen of our application. The layout is comprised of an [AppBar] @@ -167,83 +165,81 @@ class _MessageViewState extends State { } @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: _messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = _messages[index]; - if (item.user?.id == widget.channel.client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } - }, + Widget build(BuildContext context) => Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = _messages[index]; + if (item.user?.id == widget.channel.client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } + }, + ), ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - decoration: const InputDecoration( - hintText: 'Enter your message', - ), - ), - ), - Material( - type: MaterialType.circle, - color: Colors.blue, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () async { - // We can send a new message by calling `sendMessage` on - // the current channel. After sending a message, the - // TextField is cleared and the list view is scrolled - // to show the new item. - if (_controller.value.text.isNotEmpty) { - await widget.channel.sendMessage( - Message(text: _controller.value.text), - ); - _controller.clear(); - _updateList(); - } - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Center( - child: Icon( - Icons.send, - color: Colors.white, - ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: const InputDecoration( + hintText: 'Enter your message', ), ), ), - ) - ], - ), - ) - ], - ); - } + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + // We can send a new message by calling `sendMessage` on + // the current channel. After sending a message, the + // TextField is cleared and the list view is scrolled + // to show the new item. + if (_controller.value.text.isNotEmpty) { + await widget.channel.sendMessage( + Message(text: _controller.value.text), + ); + _controller.clear(); + _updateList(); + } + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ), + ) + ], + ), + ) + ], + ); } /// Helper extension for quickly retrieving diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index eee18356..94a07f34 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -888,6 +888,7 @@ class Channel { final oldMessage = state!.messages[oldIndex]; state!.updateChannelState(state!._channelState.copyWith( messages: state?.messages?..remove(oldMessage), + channel: state?._channelState.channel, )); } else { // remove thread message if present diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 9276ce22..050d36f1 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -220,27 +220,53 @@ class StreamChatClient { /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. - Future connectUser(User user, String token) => - _connectUser(user, token: Token.fromRawValue(token)); + /// Pass [connectWebSocket]: false, if you want to connect to websocket + /// at a later stage or use the client in connection-less mode + Future connectUser( + User user, + String token, { + bool connectWebSocket = true, + }) => + _connectUser( + user, + token: Token.fromRawValue(token), + connectWebSocket: connectWebSocket, + ); /// Connects the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. - Future connectUserWithProvider( - User user, TokenProvider tokenProvider) => - _connectUser(user, provider: tokenProvider); + Future connectUserWithProvider( + User user, + TokenProvider tokenProvider, { + bool connectWebSocket = true, + }) => + _connectUser( + user, + provider: tokenProvider, + connectWebSocket: connectWebSocket, + ); /// Connects the current user with an anonymous id, this triggers a connection /// to the API. It returns a [Future] that resolves when the connection is /// setup. - Future connectAnonymousUser() async { + Future connectAnonymousUser({ + bool connectWebSocket = true, + }) async { final token = Token.anonymous(); final user = OwnUser(id: token.userId); - return _connectUser(user, token: token); + return _connectUser( + user, + token: token, + connectWebSocket: connectWebSocket, + ); } /// Connects the current user as guest, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. - Future connectGuestUser(User user) async { + Future connectGuestUser( + User user, { + bool connectWebSocket = true, + }) async { final userId = user.id; final anonymousToken = Token.anonymous(userId: userId); @@ -253,13 +279,18 @@ class StreamChatClient { _tokenManager.reset(); final guestUserToken = Token.fromRawValue(guestUser.accessToken); - return _connectUser(guestUser.user, token: guestUserToken); + return _connectUser( + guestUser.user, + token: guestUserToken, + connectWebSocket: connectWebSocket, + ); } - Future _connectUser( + Future _connectUser( User user, { Token? token, TokenProvider? provider, + bool connectWebSocket = true, }) async { if (_ws.connectionCompleter?.isCompleted == false) { throw const StreamChatError( @@ -268,7 +299,7 @@ class StreamChatClient { ); } - logger.info('connecting user : ${user.id}'); + logger.info('setting user : ${user.id}'); await _tokenManager.setTokenOrProvider( user.id, @@ -279,17 +310,21 @@ class StreamChatClient { final ownUser = OwnUser.fromUser(user); state.user = ownUser; + if (!connectWebSocket) { + return ownUser; + } + try { if (_originalChatPersistenceClient != null) { _chatPersistenceClient = _originalChatPersistenceClient; await _chatPersistenceClient!.connect(ownUser.id); } - final event = await openConnection(); - return event; + final res = await openConnection(); + return res; } catch (e, stk) { if (e is StreamWebSocketError && e.isRetriable) { final event = await _chatPersistenceClient?.getConnectionInfo(); - if (event != null) return event; + if (event != null) return event.me?.merge(ownUser) ?? ownUser; } logger.severe('error connecting user : ${ownUser.id}', e, stk); rethrow; @@ -297,7 +332,7 @@ class StreamChatClient { } /// Creates a new WebSocket connection with the current user. - Future openConnection() async { + Future openConnection() async { assert( state.user != null, 'User is not set on client, ' @@ -327,7 +362,8 @@ class StreamChatClient { _ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler); try { - return await _ws.connect(user); + final event = await _ws.connect(user); + return event.me?.merge(user) ?? user; } catch (e, stk) { logger.severe('error connecting ws', e, stk); rethrow; @@ -390,7 +426,10 @@ class StreamChatClient { await queryChannelsOnline( filter: Filter.in_('cid', cids), paginationParams: const PaginationParams(limit: 30), - ).then((_) => sync(cids: cids, lastSyncAt: _lastSyncedAt)); + ); + if (persistenceEnabled) { + await sync(cids: cids, lastSyncAt: _lastSyncedAt); + } } handleEvent(Event( type: EventType.connectionRecovered, diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 7ceffbfa..679abb47 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -3,6 +3,7 @@ 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'; +import 'package:stream_chat/stream_chat.dart'; part 'own_user.g.dart'; @@ -25,6 +26,7 @@ class OwnUser extends User { bool online = false, Map extraData = const {}, bool banned = false, + List teams = const [], }) : super( id: id, role: role, @@ -34,6 +36,7 @@ class OwnUser extends User { online: online, extraData: extraData, banned: banned, + teams: teams, ); /// Create a new instance from a json @@ -50,8 +53,69 @@ class OwnUser extends User { online: user.online, banned: user.banned, extraData: user.extraData, + teams: user.teams, ); + /// Creates a copy of [OwnUser] with specified attributes overridden. + @override + OwnUser copyWith({ + String? id, + String? role, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? lastActive, + bool? online, + Map? extraData, + bool? banned, + List? teams, + List? channelMutes, + List? devices, + List? mutes, + int? totalUnreadCount, + int? unreadChannels, + }) => + OwnUser( + id: id ?? this.id, + banned: banned ?? this.banned, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + teams: teams ?? this.teams, + channelMutes: channelMutes ?? this.channelMutes, + devices: devices ?? this.devices, + mutes: mutes ?? this.mutes, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + ); + + /// Returns a new [OwnUser] that is a combination of this ownUser + /// and the given [other] ownUser. + OwnUser merge(OwnUser? other) { + if (other == null) { + return this; + } + + return copyWith( + banned: other.banned, + channelMutes: other.channelMutes, + createdAt: other.createdAt, + devices: other.devices, + extraData: other.extraData, + id: other.id, + lastActive: other.lastActive, + mutes: other.mutes, + online: other.online, + role: other.role, + teams: other.teams, + totalUnreadCount: other.totalUnreadCount, + unreadChannels: other.unreadChannels, + updatedAt: other.updatedAt, + ); + } + /// List of user devices @JsonKey(includeIfNull: false, defaultValue: []) final List devices; diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index c2960af9..1360d14e 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -47,9 +47,10 @@ class User extends Equatable { /// User role @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: []) + includeIfNull: false, + toJson: Serializer.readOnly, + defaultValue: [], + ) final List teams; /// Date of user creation diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 3c2e3d21..571d78f5 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -13,6 +13,7 @@ export './src/core/api/attachment_file_uploader.dart' 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'; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index cb3d587f..d09a2ccc 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ 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.7'; +const PACKAGE_VERSION = '2.0.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 4901adf0..1f145992 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -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.7 +version: 2.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 6883d3b1..39003fb7 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -555,7 +555,7 @@ void main() { }); test( - 'should directly update the state with message as deleted if the state is sending or failed', + '''should directly update the state with message as deleted if the state is sending or failed''', () async { const messageId = 'test-message-id'; final message = Message( @@ -981,7 +981,7 @@ void main() { ); test( - 'should override previous reaction if present and `enforceUnique` is true', + '''should override previous reaction if present and `enforceUnique` is true''', () async { const userId = 'test-user-id'; const messageId = 'test-message-id'; @@ -1875,7 +1875,7 @@ void main() { }); test( - 'should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds', + '''should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds''', () async { final typingEvent = Event(type: EventType.typingStart); diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart index 13783865..423445ec 100644 --- a/packages/stream_chat/test/src/api/client_test.dart +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -47,12 +47,6 @@ void main() { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; - final event = Event( - type: EventType.healthCheck, - connectionId: 'fake-connection-id', - me: OwnUser.fromUser(user), - ); - expectLater( // skipping first seed status -> ConnectionStatus.disconnected client.wsConnectionStatusStream.skip(1), @@ -64,9 +58,7 @@ void main() { final res = await client.connectUser(user, token); expect(res, isNotNull); - expect(res.type, event.type); - expect(res.connectionId, event.connectionId); - expect(res.me, isSameUserAs(user)); + expect(res, isSameUserAs(user)); }); test('`.connectUserWithProvider` should work fine', () async { @@ -76,12 +68,6 @@ void main() { return Token.development(userId).rawValue; } - final event = Event( - type: EventType.healthCheck, - connectionId: 'fake-connection-id', - me: OwnUser.fromUser(user), - ); - expectLater( // skipping first seed status -> ConnectionStatus.disconnected client.wsConnectionStatusStream.skip(1), @@ -93,9 +79,7 @@ void main() { final res = await client.connectUserWithProvider(user, tokenProvider); expect(res, isNotNull); - expect(res.type, event.type); - expect(res.connectionId, event.connectionId); - expect(res.me, isSameUserAs(user)); + expect(res, isSameUserAs(user)); }); group('`.connectGuestUser`', () { @@ -110,12 +94,6 @@ void main() { ..accessToken = token, ); - final event = Event( - type: EventType.healthCheck, - connectionId: 'fake-connection-id', - me: OwnUser.fromUser(user), - ); - expectLater( // skipping first seed status -> ConnectionStatus.disconnected client.wsConnectionStatusStream.skip(1), @@ -127,9 +105,7 @@ void main() { final res = await client.connectGuestUser(user); expect(res, isNotNull); - expect(res.type, event.type); - expect(res.connectionId, event.connectionId); - expect(res.me, isSameUserAs(user)); + expect(res, isSameUserAs(user)); verify( () => api.guest.getGuestUser(any(that: isSameUserAs(user))), @@ -175,9 +151,6 @@ void main() { final res = await client.connectAnonymousUser(); expect(res, isNotNull); - expect(res.type, EventType.healthCheck); - expect(res.connectionId, 'fake-connection-id'); - expect(res.me, isNotNull); }); group('`.openConnection`', () { @@ -330,6 +303,93 @@ void main() { ); }); + group('Connect user calls with `connectWebSocket`: false', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + client = StreamChatClient(apiKey, chatApi: api); + }); + + tearDown(() { + client.dispose(); + }); + + test('`.connectUser` should succeed without connecting', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + final res = await client.connectUser( + user, + token, + connectWebSocket: false, + ); + expect(res, isSameUserAs(user)); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + }); + + test( + '`.connectUserWithProvider` should succeed without connecting', + () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + final res = await client.connectUserWithProvider( + user, + tokenProvider, + connectWebSocket: false, + ); + expect(res, isSameUserAs(user)); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + }, + ); + + test('`.connectGuestUser` should succeed without connecting', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + final res = await client.connectGuestUser( + user, + connectWebSocket: false, + ); + + expect(res, isSameUserAs(user)); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + + test( + '`.connectAnonymousUser` should succeed without connecting', + () async { + final res = await client.connectAnonymousUser( + connectWebSocket: false, + ); + + expect(res, isNotNull); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + }, + ); + }); + group('Fake web-socket connection function with failure and persistence', () { const apiKey = 'test-api-key'; late final api = FakeChatApi(); @@ -353,7 +413,7 @@ void main() { }); test( - '`.connectUser` should connect successfully if persistence contains event', + '''`.connectUser` should connect successfully if persistence contains event''', () async { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; @@ -366,8 +426,7 @@ void main() { final res = await client.connectUser(user, token); expect(res, isNotNull); - expect(res.connectionId, 'test-connection-id'); - expect(res.me?.id, user.id); + expect(res, isSameUserAs(user)); verify(persistence.getConnectionInfo).called(1); verifyNoMoreInteractions(persistence); @@ -375,7 +434,7 @@ void main() { ); test( - '`.connectUserWithProvider` should connect successfully if persistence contains event', + '''`.connectUserWithProvider` should connect successfully if persistence contains event''', () async { final user = User(id: 'test-user-id'); Future tokenProvider(String userId) async { @@ -391,8 +450,7 @@ void main() { final res = await client.connectUserWithProvider(user, tokenProvider); expect(res, isNotNull); - expect(res.connectionId, 'test-connection-id'); - expect(res.me?.id, user.id); + expect(res, isSameUserAs(user)); verify(persistence.getConnectionInfo).called(1); verifyNoMoreInteractions(persistence); @@ -400,7 +458,7 @@ void main() { ); test( - '`.connectGuestUser` should connect successfully if persistence contains event', + '''`.connectGuestUser` should connect successfully if persistence contains event''', () async { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; @@ -420,8 +478,7 @@ void main() { final res = await client.connectGuestUser(user); expect(res, isNotNull); - expect(res.connectionId, 'test-connection-id'); - expect(res.me?.id, user.id); + expect(res, isSameUserAs(user)); verify(persistence.getConnectionInfo).called(1); verifyNoMoreInteractions(persistence); @@ -432,7 +489,7 @@ void main() { ); test( - '`.connectAnonymousUser` should connect successfully if persistence contains event', + '''`.connectAnonymousUser` should connect successfully if persistence contains event''', () async { final user = User(id: 'test-user-id'); @@ -446,8 +503,6 @@ void main() { final res = await client.connectAnonymousUser(); expect(res, isNotNull); - expect(res.connectionId, 'test-connection-id'); - expect(res.me?.id, user.id); verify(persistence.getConnectionInfo).called(1); verifyNoMoreInteractions(persistence); @@ -488,7 +543,7 @@ void main() { group('`.sync`', () { test( - 'should update persistence connectionInfo and lastSync when sync succeeds', + '''should update persistence connectionInfo and lastSync when sync succeeds''', () async { const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; final lastSyncAt = DateTime.now(); @@ -662,7 +717,7 @@ void main() { ); test( - 'should never rethrow network call if persistence already emitted some channels', + '''should never rethrow network call if persistence already emitted some channels''', () async { final persistentChannelStates = List.generate( 3, @@ -870,7 +925,7 @@ void main() { }); test( - 'should rethrow if `.queryChannelsOnline` throws and persistence channels are empty', + '''should rethrow if `.queryChannelsOnline` throws and persistence channels are empty''', () async { when(() => api.channel.queryChannels( filter: any(named: 'filter'), diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index ece00efd..ab4c4984 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -57,7 +57,7 @@ void main() { }); test( - 'connectionIdInterceptor should be added if connectionIdManager is provided', + '''connectionIdInterceptor should be added if connectionIdManager is provided''', () { const apiKey = 'api-key'; final client = StreamHttpClient( diff --git a/packages/stream_chat/test/src/core/http/token_manager_test.dart b/packages/stream_chat/test/src/core/http/token_manager_test.dart index a1a38fa5..163bafff 100644 --- a/packages/stream_chat/test/src/core/http/token_manager_test.dart +++ b/packages/stream_chat/test/src/core/http/token_manager_test.dart @@ -45,7 +45,7 @@ void main() { }); test( - '`setTokenOrProvider` should throw if both token and provider is not provided', + '''`setTokenOrProvider` should throw if both token and provider is not provided''', () async { expect(tokenManager.userId, isNull); diff --git a/packages/stream_chat/test/src/core/models/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart index 663ab702..d60ebb50 100644 --- a/packages/stream_chat/test/src/core/models/filter_test.dart +++ b/packages/stream_chat/test/src/core/models/filter_test.dart @@ -192,7 +192,7 @@ void main() { test('custom with no operator', () { const key = 'testKey'; const values = ['testValue']; - final filter = Filter.custom(key: key, value: values); + const filter = Filter.custom(key: key, value: values); final encoded = json.encode(filter); expect( encoded, diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index edaf022e..7aef859d 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -41,5 +41,43 @@ void main() { expect(ownUser.banned, user.banned); expect(ownUser.extraData, user.extraData); }); + + test('copyWith', () { + final user = OwnUser.fromJson(jsonFixture('own_user.json')); + var newUser = user.copyWith(); + + expect(newUser.id, user.id); + expect(newUser.role, user.role); + expect(newUser.name, user.name); + + newUser = user.copyWith( + id: 'test', + role: 'test', + extraData: { + 'name': 'test', + }, + ); + + expect(newUser.id, 'test'); + expect(newUser.role, 'test'); + expect(newUser.name, 'test'); + }); + + test('merge', () { + final user = OwnUser.fromJson(jsonFixture('own_user.json')); + final newUser = user.merge(OwnUser( + id: 'test', + role: 'test', + extraData: const { + 'name': 'test', + }, + banned: true, + )); + + expect(newUser.id, 'test'); + expect(newUser.role, 'test'); + expect(newUser.name, 'test'); + expect(newUser.banned, true); + }); }); } diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 518a223e..0891b548 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -13,7 +13,7 @@ void main() { expect(reaction.type, 'wow'); expect( reaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: { + User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { 'image': 'https://randomuser.me/api/portraits/women/45.jpg', 'name': 'Daisy Morgan' }).toJson(), @@ -28,7 +28,8 @@ void main() { messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), type: 'wow', - user: User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: { + user: + User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { 'image': 'https://randomuser.me/api/portraits/women/45.jpg', 'name': 'Daisy Morgan' }), diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 0ab7695f..22aee1a9 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -154,7 +154,6 @@ void main() { }); test('updateChannelState', () async { - const cid = 'test:cid'; final channelState = ChannelState(); persistenceClient.updateChannelState(channelState); }); diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 3407178c..e606d32d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,88 @@ +## 2.0.0 + +🛑️ Breaking Changes from `1.5.4` + +- Migrate this package to null safety +- Renamed `ChannelImage` to `ChannelAvatar` +- Updated `StreamChatThemeData.reactionIcons` to accept custom builder +- Renamed `ColorTheme` properties to reflect the purpose of the colors + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + +- `ChannelListCore` options property is removed in favor of individual properties + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence +- `UserListView` options property is removed in favor of individual properties + - `options.presence` -> bool presence +- Renamed `ImageHeader` to `GalleryHeader` +- Renamed `ImageFooter` to `GalleryFooter` +- `MessageBuilder` and `ParentMessageBuilder` signature is now + +```dart +typedef MessageBuilder = Widget Function( + BuildContext, + MessageDetails, + List, + MessageWidget defaultMessageWidget, + ); +``` + +the last parameter is the default `MessageWidget` +You can call `.copyWith` to customize just a subset of properties + + +✅ Added + +- Added video compress options (frame and quality) to `MessageInput` +- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- `MessageWidget` accepts a `userAvatarBuilder` +- Added pinMessage ui support +- Added `MessageListView.threadSeparatorBuilder` property +- Added `MessageInput.onError` property to allow error handling +- Added `GalleryHeader/GalleryFooter` theme classes + +🐞 Fixed + +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- `MessageListView` not rendering if the user is not a member of the channel +- Fix `MessageInput` overflow when there are no actions +- Minor fixes and improvements + +## 2.0.0-nullsafety.9 + +🛑️ Breaking Changes from `2.0.0-nullsafety.8` + +- Renamed `ColorTheme` properties to reflect the purpose of the colors + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + +✅ Added + +- Added video compress options (frame and quality) to `MessageInput` + ## 2.0.0-nullsafety.8 🛑️ Breaking Changes from `2.0.0-nullsafety.7` diff --git a/packages/stream_chat_flutter/README.md b/packages/stream_chat_flutter/README.md index 44f7582b..41ea079d 100644 --- a/packages/stream_chat_flutter/README.md +++ b/packages/stream_chat_flutter/README.md @@ -28,7 +28,7 @@ It teaches you how to use this SDK and also shows how to make frequently require ## Example App This repo includes a fully functional example app with setup instructions. -The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/example) folder. +The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter/example) folder. ## Add dependency Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index c1246a31..91d44176 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -34,84 +34,96 @@ class _HindiStreamChatLocalizationsDelegate void main() async { WidgetsFlutterBinding.ensureInitialized(); - /// Create a new instance of [StreamChatClient] passing the apikey obtained from your - /// project dashboard. + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, ); - /// Set the current user and connect the websocket. In a production scenario, this should be done using - /// a backend to generate a user token using our server SDK. + /// Set the current user and connect the websocket. In a production + /// scenario, this should be done using a backend to generate a user token + /// using our server SDK. + /// /// Please see the following for more information: /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' + '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', ); final channel = client.channel('messaging', id: 'godevs'); await channel.watch(); - runApp(MyApp(client, channel)); + runApp( + MyApp( + client: client, + channel: channel, + ), + ); } /// Example application using Stream Chat Flutter widgets. -/// Stream Chat Flutter is a set of Flutter widgets which provide full chat functionalities -/// for building Flutter applications using Stream. -/// If you'd prefer using minimal wrapper widgets for your app, please see our other +/// +/// Stream Chat Flutter is a set of Flutter widgets which provide full chat +/// functionalities for building Flutter applications using Stream. If you'd +/// prefer using minimal wrapper widgets for your app, please see our other /// package, `stream_chat_flutter_core`. class MyApp extends StatelessWidget { + /// Example using Stream's Flutter package. + /// + /// If you'd prefer using minimal wrapper widgets for your app, please see + /// our other package, `stream_chat_flutter_core`. + const MyApp({ + Key? key, + required this.client, + required this.channel, + }) : super(key: key); + /// Instance of Stream Client. - /// Stream's [StreamChatClient] can be used to connect to our servers and set the default - /// user for the application. Performing these actions trigger a websocket connection - /// allowing for real-time updates. + /// + /// Stream's [StreamChatClient] can be used to connect to our servers and + /// set the default user for the application. Performing these actions + /// trigger a websocket connection allowing for real-time updates. final StreamChatClient client; /// Instance of the Channel final Channel channel; - /// Example using Stream's Flutter package. - /// If you'd prefer using minimal wrapper widgets for your app, please see our other - /// package, `stream_chat_flutter_core`. - MyApp(this.client, this.channel); - @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - supportedLocales: [ - Locale('en', 'US'), - Locale('hi', 'IN'), - ], - localizationsDelegates: [ - GlobalStreamChatLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - StreamChatLocalizationsHi.delegate, - ], - builder: (context, widget) { - return StreamChat( + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, + supportedLocales: [ + Locale('en', 'US'), + Locale('hi', 'IN'), + ], + localizationsDelegates: [ + GlobalStreamChatLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + StreamChatLocalizationsHi.delegate, + ], + builder: (context, widget) => StreamChat( client: client, child: widget, - ); - }, - home: StreamChannel( - channel: channel, - child: ChannelPage(), - ), - ); - } + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); } /// A list of messages sent in the current channel. /// -/// This is implemented using [MessageListView], a widget that provides query functionalities -/// fetching the messages from the api and showing them in a listView +/// This is implemented using [MessageListView], a widget that provides query +/// functionalities fetching the messages from the api and showing them in a +/// listView. class ChannelPage extends StatelessWidget { /// Creates the page that shows the list of messages const ChannelPage({ @@ -119,17 +131,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 1c833e23..454071a6 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -10,30 +10,39 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + final StreamChatClient client; - MyApp(this.client); - @override - Widget build(BuildContext context) { - return MaterialApp( - builder: (context, child) => StreamChat( - client: client, - child: child, - ), - home: SplitView(), - ); - } + Widget build(BuildContext context) => MaterialApp( + builder: (context, child) => StreamChat( + client: client, + child: child, + ), + home: const SplitView(), + ); } class SplitView extends StatefulWidget { + const SplitView({ + Key? key, + }) : super(key: key); + @override _SplitViewState createState() => _SplitViewState(); } @@ -42,69 +51,67 @@ class _SplitViewState extends State { Channel? selectedChannel; @override - Widget build(BuildContext context) { - return Flex( - direction: Axis.horizontal, - children: [ - Flexible( - flex: 1, - child: ChannelListPage( - onTap: (channel) { - setState(() { - selectedChannel = channel; - }); - }, + Widget build(BuildContext context) => Flex( + direction: Axis.horizontal, + children: [ + Flexible( + child: ChannelListPage( + onTap: (channel) { + setState(() { + selectedChannel = channel; + }); + }, + ), ), - ), - Flexible( - flex: 2, - child: Scaffold( - body: selectedChannel != null - ? StreamChannel( - key: ValueKey(selectedChannel!.cid), - channel: selectedChannel!, - child: ChannelPage(), - ) - : Center( - child: Text( - 'Pick a channel to show the messages 💬', - style: Theme.of(context).textTheme.headline5, + Flexible( + flex: 2, + child: Scaffold( + body: selectedChannel != null + ? StreamChannel( + key: ValueKey(selectedChannel!.cid), + channel: selectedChannel!, + child: const ChannelPage(), + ) + : Center( + child: Text( + 'Pick a channel to show the messages 💬', + style: Theme.of(context).textTheme.headline5, + ), ), - ), + ), ), - ), - ], - ); - } + ], + ); } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + this.onTap, + }) : super(key: key); + final void Function(Channel)? onTap; - ChannelListPage({this.onTap}); - @override - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - onChannelTap: onTap != null - ? (channel, _) { - onTap!(channel); - } - : null, - filter: Filter.in_( - 'members', - [StreamChat.of(context).user!.id], - ), - sort: [SortOption('last_message_at')], - pagination: PaginationParams( - limit: 20, + Widget build(BuildContext context) => Scaffold( + body: ChannelsBloc( + child: ChannelListView( + onChannelTap: onTap != null + ? (channel, _) { + onTap!(channel); + } + : null, + filter: Filter.in_( + 'members', + [StreamChat.of(context).user!.id], + ), + sort: const [SortOption('last_message_at')], + pagination: const PaginationParams( + limit: 20, + ), ), ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -113,27 +120,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { - return Navigator( - onGenerateRoute: (settings) { - return MaterialPageRoute( - builder: (context) { - return Scaffold( - appBar: ChannelHeader( - showBackButton: false, - ), - body: Column( - children: [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - }, - ); - }, - ); - } + Widget build(BuildContext context) => Navigator( + onGenerateRoute: (settings) => MaterialPageRoute( + builder: (context) => Scaffold( + appBar: const ChannelHeader( + showBackButton: false, + ), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ), + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart similarity index 67% rename from packages/stream_chat_flutter/example/lib/tutorial-part-1.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 2a720c70..459d8690 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -4,25 +4,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// First step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// -/// There are three important things to notice that are common to all Flutter application using StreamChat: +/// There are three important things to notice that are common to all Flutter +/// application using StreamChat: /// /// 1. The Dart API [StreamChatClient] is initialized with your API Key /// 2. The current user is set by calling [StreamChatClient.connectUser] /// 3. The client is then passed to the top-level [StreamChat] widget -/// [StreamChat] is an inherited widget and must be the parent of all Chat related widgets. +/// [StreamChat] is an inherited widget and must be the parent of all +/// Chat related widgets. /// -/// Please note that while Flutter can be used to build both mobile and web applications; -/// in this tutorial we focus on mobile, make sure when running the app you use a mobile device. +/// Please note that while Flutter can be used to build both mobile and web +/// applications, in this tutorial we focus on mobile. Make sure when running +/// the app that you use a mobile device. /// /// Let's have a look at what we've built: /// /// - We set up the Chat [StreamChatClient] with the API key /// -/// - We set the the current user for Chat with [StreamChatClient.connectUser] and a pre-generated user token +/// - We set the the current user for Chat with [StreamChatClient.connectUser] +/// and a pre-generated user token /// /// - We make [StreamChat] the root Widget of our application /// -/// - We create a single [ChannelPage] widget under [StreamChat] with three widgets: [ChannelHeader], [MessageListView] and [MessageInput] +/// - We create a single [ChannelPage] widget under [StreamChat] with three +/// widgets: [ChannelHeader], [MessageListView] and [MessageInput] /// /// If you now run the simulator you will see a single channel UI. void main() async { @@ -33,26 +38,38 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); - // ignore: unawaited_futures + // ignore: unawaited_futures, cascade_invocations channel.watch(); - runApp(MyApp(client, channel)); + runApp( + MyApp( + client: client, + channel: channel, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + required this.channel, + }) : super(key: key); + final StreamChatClient client; + final Channel channel; - MyApp(this.client, this.channel); - @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( + // ignore: prefer_expression_function_bodies builder: (context, widget) { return StreamChat( client: client, @@ -61,7 +78,7 @@ class MyApp extends StatelessWidget { }, home: StreamChannel( channel: channel, - child: ChannelPage(), + child: const ChannelPage(), ), ); } @@ -73,11 +90,12 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( - children: [ + children: const [ Expanded( child: MessageListView(), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart similarity index 54% rename from packages/stream_chat_flutter/example/lib/tutorial-part-2.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 1e198d61..79f8674c 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -5,20 +5,29 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Second step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// /// Most chat applications handle more than just one single conversation. -/// Apps like Facebook Messenger, Whatsapp and Telegram allows you to have multiple one to one and group conversations. +/// Apps like Facebook Messenger, Whatsapp and Telegram allows you to have +/// multiple one-to-one and group conversations. /// -/// Let’s find out how we can change our application chat screen to display the list of conversations and navigate between them. +/// Let’s find out how we can change our application chat screen to display +/// the list of conversations and navigate between them. /// -/// > Note: the SDK uses Flutter’s [Navigator] to move from one route to another, this allows us to avoid any boiler-plate code. -/// > Of course you can take total control of how navigation works by customizing widgets like [Channel] and [ChannelList]. +/// > Note: the SDK uses Flutter’s [Navigator] to move from one route to +/// another. This allows us to avoid any boiler-plate code. +/// > Of course, you can take total control of how navigation works by +/// customizing widgets like [Channel] and [ChannelList]. /// -/// If you run the application, you will see that the first screen shows a list of conversations, you can open each by tapping and go back to the list. +/// If you run the application, you will see that the first screen shows a +/// list of conversations, you can open each by tapping and go back to the list. /// -/// Every single widget involved in this UI can be customized or swapped with your own. +/// Every single widget involved in this UI can be customized or swapped +/// with your own. /// -/// The [ChannelListPage] widget retrieves the list of channels based on a custom query and ordering. -/// In this case we are showing the list of channels the current user is a member and we order them based on the time they had a new message. -/// [ChannelListView] handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel. +/// The [ChannelListPage] widget retrieves the list of channels based on a +/// custom query and ordering. In this case we are showing the list of +/// channels in which the current user is a member and we order them based +/// on the time they had a new message. [ChannelListView] handles pagination +/// and updates automatically when new channels are created or when a new +/// message is added to a channel. void main() async { final client = StreamChatClient( 's2dxdhpxd94g', @@ -27,31 +36,44 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + final StreamChatClient client; - MyApp(this.client); - @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( client: client, child: child, ), - home: ChannelListPage(), + home: const ChannelListPage(), ); } } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( body: ChannelsBloc( @@ -60,11 +82,11 @@ class ChannelListPage extends StatelessWidget { 'members', [StreamChat.of(context).user!.id], ), - sort: [SortOption('last_message_at')], - pagination: PaginationParams( + sort: const [SortOption('last_message_at')], + pagination: const PaginationParams( limit: 20, ), - channelWidget: ChannelPage(), + channelWidget: const ChannelPage(), ), ), ); @@ -77,11 +99,12 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( - children: [ + children: const [ Expanded( child: MessageListView(), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart similarity index 68% rename from packages/stream_chat_flutter/example/lib/tutorial-part-3.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index fc55f6d9..78081c85 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -6,22 +6,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Third step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// /// So far you’ve learned how to use the default widgets. -/// The library has been designed with composition in mind and to allow all common customizations to be very easy. -/// This means that you can change any component in your application by swapping the default widgets with the ones you build yourself. +/// The library has been designed with composition in mind and to allow all +/// common customizations to be very easy. +/// This means that you can change any component in your application by +/// swapping the default widgets with the ones you build yourself. /// /// Let’s see how we can make some changes to the SDK’s UI components. -/// We start by changing how channel previews are shown in the channel list and include the number of unread messages for each. +/// We start by changing how channel previews are shown in the channel list +/// and include the number of unread messages for each. /// -/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder], this will override the default [ChannelPreview] and allows you to create one yourself. +/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder]; +/// this will override the default [ChannelPreview] and allows you to create +/// one yourself. /// /// There are a couple interesting things we do in this widget: /// -/// - Instead of creating a whole new style for the channel name, we inherit the text style from the parent theme ([StreamChatTheme.of]) and only change the color attribute +/// - Instead of creating a whole new style for the channel name, we inherit +/// the text style from the parent theme ([StreamChatTheme.of]) and only +/// change the color attribute /// -/// - We loop over the list of channel messages to search for the first not deleted message ([Channel.state.messages]) +/// - We loop over the list of channel messages to search for the first not +/// deleted message ([Channel.state.messages]) /// /// - We retrieve the count of unread messages from [Channel.state] -void main() async { +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -29,31 +37,44 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + final StreamChatClient client; - MyApp(this.client); - @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( client: client, child: child, ), - home: ChannelListPage(), + home: const ChannelListPage(), ); } } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( body: ChannelsBloc( @@ -64,10 +85,10 @@ class ChannelListPage extends StatelessWidget { ), channelPreviewBuilder: _channelPreviewBuilder, // sort: [SortOption('last_message_at')], - pagination: PaginationParams( + pagination: const PaginationParams( limit: 20, ), - channelWidget: ChannelPage(), + channelWidget: const ChannelPage(), ), ), ); @@ -78,7 +99,7 @@ class ChannelListPage extends StatelessWidget { (message) => !message.isDeleted, ); - final subtitle = (lastMessage == null ? 'nothing yet' : lastMessage.text!); + final subtitle = lastMessage == null ? 'nothing yet' : lastMessage.text!; final opacity = (channel.state?.unreadCount ?? 0) > 0 ? 1.0 : 0.5; return ListTile( @@ -88,12 +109,12 @@ class ChannelListPage extends StatelessWidget { MaterialPageRoute( builder: (_) => StreamChannel( channel: channel, - child: ChannelPage(), + child: const ChannelPage(), ), ), ); }, - leading: ChannelImage( + leading: ChannelAvatar( channel: channel, ), title: ChannelName( @@ -101,17 +122,17 @@ class ChannelListPage extends StatelessWidget { StreamChatTheme.of(context).channelPreviewTheme.title!.copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(opacity), ), ), subtitle: Text(subtitle), - trailing: channel.state!.unreadCount! > 0 + trailing: channel.state!.unreadCount > 0 ? CircleAvatar( radius: 10, child: Text(channel.state!.unreadCount.toString()), ) - : SizedBox(), + : const SizedBox(), ); } } @@ -122,11 +143,12 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( - children: [ + children: const [ Expanded( child: MessageListView(), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart similarity index 61% rename from packages/stream_chat_flutter/example/lib/tutorial-part-4.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 5a85339f..07942302 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -4,13 +4,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Fourth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// -/// Stream Chat supports message threads out of the box. Threads allows users to create sub-conversations inside the same channel. +/// Stream Chat supports message threads out of the box. Threads allows users +/// to create sub-conversations inside the same channel. /// -/// Using threaded conversations is very simple and mostly a matter of plugging the [MessageListView] to another widget that renders the widget. -/// To make this simple, such a widget only needs to build [MessageListView] with the parent attribute set to the thread’s root message. +/// Using threaded conversations is very simple and mostly a matter of +/// plugging the [MessageListView] to another widget that renders the widget. +/// To make this simple, such a widget only needs to build [MessageListView] +/// with the parent attribute set to the thread’s root message. /// -/// Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same [ThreadPage]. -void main() async { +/// Now we can open threads and create new ones as well. If you long-press a +/// message, you can tap on "Reply" and it will open the same [ThreadPage]. +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -18,33 +22,44 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + final StreamChatClient client; - MyApp(this.client); - @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( client: client, child: child, ), - home: Container( - child: ChannelListPage(), - ), + home: const ChannelListPage(), ); } } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( body: ChannelsBloc( @@ -53,11 +68,11 @@ class ChannelListPage extends StatelessWidget { 'members', [StreamChat.of(context).user!.id], ), - sort: [SortOption('last_message_at')], - pagination: PaginationParams( + sort: const [SortOption('last_message_at')], + pagination: const PaginationParams( limit: 20, ), - channelWidget: ChannelPage(), + channelWidget: const ChannelPage(), ), ), ); @@ -70,21 +85,20 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( children: [ Expanded( child: MessageListView( - threadBuilder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - ); - }, + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - MessageInput(), + const MessageInput(), ], ), ); @@ -92,14 +106,15 @@ class ChannelPage extends StatelessWidget { } class ThreadPage extends StatelessWidget { - final Message? parent; - - ThreadPage({ + const ThreadPage({ Key? key, this.parent, }) : super(key: key); + final Message? parent; + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( appBar: ThreadHeader( diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart similarity index 60% rename from packages/stream_chat_flutter/example/lib/tutorial-part-5.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 8f1118dc..dbbf31a0 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -4,18 +4,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Fifth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// -/// Customizing how messages are rendered is another very common use-case that the SDK supports easily. +/// Customizing how messages are rendered is another very common use-case that +/// the SDK supports easily. /// -/// Replace the built-in message component with your own is done by passing it as a builder function to the [MessageListView] widget. +/// Replacing the built-in message component with your own is done by passing +/// it as a builder function to the [MessageListView] widget. /// -/// The message builder function will get the usual [BuildContext] argument as well as the [Message] object and its position inside the list. +/// The message builder function will get the usual [BuildContext] argument +/// as well as the [Message] object and its position inside the list. /// -/// If you look at the code you can see that we use [StreamChat.of] to retrieve the current user so that we can style messages own messages in a different way. +/// If you look at the code you can see that we use [StreamChat.of] to +/// retrieve the current user so that we can style messages in a different way. /// -/// Since custom widgets and builders are always children of [StreamChat] or part of a [Channel], -/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly -/// or to retrieve outer scope needed such as messages from the [Channel.state]. -void main() async { +/// Since custom widgets and builders are always children of [StreamChat] or +/// part of a [Channel], you can use [StreamChat.of], [StreamChannel.of], +/// and [StreamChatTheme.of] to use the API client directly or to retrieve +/// outer scope needed such as messages from the [Channel.state]. +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -23,31 +28,44 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + final StreamChatClient client; - MyApp(this.client); - @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( client: client, child: child, ), - home: ChannelListPage(), + home: const ChannelListPage(), ); } } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( body: ChannelsBloc( @@ -56,11 +74,11 @@ class ChannelListPage extends StatelessWidget { 'members', [StreamChat.of(context).user!.id], ), - sort: [SortOption('last_message_at')], - pagination: PaginationParams( + sort: const [SortOption('last_message_at')], + pagination: const PaginationParams( limit: 20, ), - channelWidget: ChannelPage(), + channelWidget: const ChannelPage(), ), ), ); @@ -73,9 +91,10 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( children: [ Expanded( @@ -83,7 +102,7 @@ class ChannelPage extends StatelessWidget { messageBuilder: _messageBuilder, ), ), - MessageInput(), + const MessageInput(), ], ), ); @@ -101,12 +120,14 @@ class ChannelPage extends StatelessWidget { final color = isCurrentUser ? Colors.blueGrey : Colors.blue; return Padding( - padding: EdgeInsets.all(5.0), + padding: const EdgeInsets.all(5), child: Container( decoration: BoxDecoration( - border: Border.all(color: color, width: 1), - borderRadius: BorderRadius.all( - Radius.circular(5.0), + border: Border.all( + color: color, + ), + borderRadius: const BorderRadius.all( + Radius.circular(5), ), ), child: ListTile( @@ -115,7 +136,7 @@ class ChannelPage extends StatelessWidget { textAlign: textAlign, ), subtitle: Text( - message.user!.extraData['name'] as String, + message.user!.name, textAlign: textAlign, ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart similarity index 60% rename from packages/stream_chat_flutter/example/lib/tutorial-part-6.dart rename to packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index c49ffaf1..fcf86b5c 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -4,22 +4,29 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Sixth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) /// -/// The Flutter SDK comes with a fully designed set of widgets which you can customize to fit with your application style and typography. -/// Changing the theme of Chat widgets works in a very similar way that [MaterialApp] and [Theme] do. +/// The Flutter SDK comes with a fully designed set of widgets which you can +/// customize to fit with your application style and typography. +/// Changing the theme of Chat widgets works in a very similar way that +/// [MaterialApp] and [Theme] do. /// -/// Out of the box all chat widgets use their own default styling, there are two ways to change the styling: +/// All chat widgets use their own default styling out of the box. There are +/// two ways to change the styling: /// /// 1. Initialize the [StreamChatTheme] from your existing [MaterialApp] style /// 2. Construct a custom theme and provide all the customizations needed /// -/// First we create a new Material [Theme] and pick [Colors.green] as swatch color. The theme is then passed to [MaterialApp] as usual. +/// First, we create a new Material [Theme] and pick [Colors.green] as the +/// swatch color. The theme is then passed to [MaterialApp] as usual. /// -/// Then we create a new [StreamChatTheme] from the green theme we just created. -/// After saving the app you will see the UI will update several widgets to match with the new color. +/// Then, we create a new [StreamChatTheme] from the green theme we just +/// created. After saving the app you will see that several widgets have +/// been updated with the new color. /// /// We also change the message color posted by the current user. -/// You can perform these more granular style changes using [StreamChatTheme.copyWith]. -void main() async { +/// +/// You can perform these more granular style changes using +/// [StreamChatTheme.copyWith]. +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -27,16 +34,23 @@ void main() async { await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); - runApp(MyApp(client)); + runApp( + MyApp( + client: client, + ), + ); } class MyApp extends StatelessWidget { - final StreamChatClient client; + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); - MyApp(this.client); + final StreamChatClient client; @override Widget build(BuildContext context) { @@ -50,9 +64,9 @@ class MyApp extends StatelessWidget { ), ), otherMessageTheme: MessageTheme( - messageBackgroundColor: colorTheme.black, + messageBackgroundColor: colorTheme.textHighEmphasis, messageText: TextStyle( - color: colorTheme.white, + color: colorTheme.barsBg, ), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(8), @@ -62,20 +76,23 @@ class MyApp extends StatelessWidget { return MaterialApp( theme: themeData, - builder: (context, child) { - return StreamChat( - client: client, - streamChatThemeData: customTheme, - child: child, - ); - }, - home: ChannelListPage(), + builder: (context, child) => StreamChat( + client: client, + streamChatThemeData: customTheme, + child: child, + ), + home: const ChannelListPage(), ); } } class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( body: ChannelsBloc( @@ -84,11 +101,11 @@ class ChannelListPage extends StatelessWidget { 'members', [StreamChat.of(context).user!.id], ), - sort: [SortOption('last_message_at')], - pagination: PaginationParams( + sort: const [SortOption('last_message_at')], + pagination: const PaginationParams( limit: 20, ), - channelWidget: ChannelPage(), + channelWidget: const ChannelPage(), ), ), ); @@ -101,21 +118,20 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ChannelHeader(), + appBar: const ChannelHeader(), body: Column( children: [ Expanded( child: MessageListView( - threadBuilder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - ); - }, + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - MessageInput(), + const MessageInput(), ], ), ); @@ -123,14 +139,15 @@ class ChannelPage extends StatelessWidget { } class ThreadPage extends StatelessWidget { - final Message? parent; - - ThreadPage({ + const ThreadPage({ Key? key, this.parent, }) : super(key: key); + final Message? parent; + @override + // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( appBar: ThreadHeader( diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 4eb99d86..d3fec032 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -21,23 +21,23 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + collection: ^1.15.0 + cupertino_icons: ^1.0.3 flutter: sdk: flutter -# stream_chat: -# path: ../../stream_chat -# stream_chat_flutter_core: -# path: ../../stream_chat_flutter_core + # stream_chat: + # path: ../../stream_chat + # stream_chat_flutter_core: + # path: ../../stream_chat_flutter_core stream_chat_flutter: path: ../ stream_chat_localizations: path: ../../stream_chat_localizations - stream_chat_persistence: + stream_chat_persistence: path: ../../stream_chat_persistence - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - collection: ^1.15.0 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index ba369e5d..b068a8f9 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -35,7 +35,7 @@ class AttachmentTitle extends StatelessWidget { attachment.title!, overflow: TextOverflow.ellipsis, style: messageTheme.messageText?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index ebbaed7d..af46515e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -130,7 +130,7 @@ class _PreparingState extends StatelessWidget { alignment: Alignment.topRight, child: _IconButton( icon: StreamSvgIcon.close( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -170,7 +170,7 @@ class _InProgressState extends StatelessWidget { alignment: Alignment.topRight, child: _IconButton( icon: StreamSvgIcon.close( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -209,7 +209,7 @@ class _FailedState extends StatelessWidget { children: [ _IconButton( icon: StreamSvgIcon.retry( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), onPressed: () { channel.retryAttachmentUpload(messageId, attachmentId); @@ -229,7 +229,7 @@ class _FailedState extends StatelessWidget { child: Text( context.translations.uploadErrorLabel, style: theme.textTheme.footnote.copyWith( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), ), ), @@ -248,7 +248,7 @@ class _SuccessState extends StatelessWidget { backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, maxRadius: 12, child: StreamSvgIcon.check( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index ee79d959..e92087b5 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -75,12 +75,14 @@ class AttachmentError extends StatelessWidget { child: Container( width: size?.width, height: size?.height, - color: - StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1), + color: StreamChatTheme.of(context) + .colorTheme + .accentError + .withOpacity(.1), child: Center( child: Icon( Icons.error_outline, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 547c5821..7377ba20 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -54,10 +54,10 @@ class FileAttachment extends AttachmentWidget { width: size?.width ?? 100, height: 56, decoration: BoxDecoration( - color: colorTheme.white, + color: colorTheme.barsBg, borderRadius: BorderRadius.circular(12), border: Border.all( - color: colorTheme.greyWhisper, + color: colorTheme.borders, ), ), child: Row( @@ -141,8 +141,8 @@ class FileAttachment extends AttachmentWidget { final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, @@ -217,7 +217,7 @@ class FileAttachment extends AttachmentWidget { preparing: () => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.close(color: theme.colorTheme.white), + icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -225,7 +225,7 @@ class FileAttachment extends AttachmentWidget { inProgress: (_, __) => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.close(color: theme.colorTheme.white), + icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -233,15 +233,15 @@ class FileAttachment extends AttachmentWidget { success: () => Padding( padding: const EdgeInsets.all(8), child: CircleAvatar( - backgroundColor: theme.colorTheme.accentBlue, + backgroundColor: theme.colorTheme.accentPrimary, maxRadius: 12, - child: StreamSvgIcon.check(color: theme.colorTheme.white), + child: StreamSvgIcon.check(color: theme.colorTheme.barsBg), ), ), failed: (_) => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.retry(color: theme.colorTheme.white), + icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.retryAttachmentUpload( message.id, @@ -253,7 +253,8 @@ class FileAttachment extends AttachmentWidget { if (message.status == MessageSendingStatus.sent) { trailingWidget = IconButton( - icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black), + icon: StreamSvgIcon.cloudDownload( + color: theme.colorTheme.textHighEmphasis), visualDensity: VisualDensity.compact, splashRadius: 16, onPressed: () { @@ -272,7 +273,7 @@ class FileAttachment extends AttachmentWidget { final theme = StreamChatTheme.of(context); final size = attachment.file?.size ?? attachment.extraData['file_size']; final textStyle = theme.textTheme.footnote.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ); return attachment.uploadState.when( preparing: () => Text(fileSize(size), style: textStyle), @@ -282,7 +283,7 @@ class FileAttachment extends AttachmentWidget { showBackground: false, padding: EdgeInsets.zero, textStyle: textStyle, - progressIndicatorColor: theme.colorTheme.accentBlue, + progressIndicatorColor: theme.colorTheme.accentPrimary, ), success: () => Text(fileSize(size), style: textStyle), failed: (_) => Text( diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1ae03ca1..4264c5f1 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -52,7 +52,7 @@ class GiphyAttachment extends AttachmentWidget { mainAxisSize: MainAxisSize.min, children: [ Card( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, elevation: 2, clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( @@ -84,7 +84,7 @@ class GiphyAttachment extends AttachmentWidget { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), overflow: TextOverflow.ellipsis, @@ -118,7 +118,7 @@ class GiphyAttachment extends AttachmentWidget { Container( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), width: double.infinity, height: 0.5, @@ -142,7 +142,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), @@ -153,7 +153,7 @@ class GiphyAttachment extends AttachmentWidget { width: 0.5, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), height: 50, ), @@ -174,7 +174,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), maxLines: 1, @@ -186,7 +186,7 @@ class GiphyAttachment extends AttachmentWidget { width: 0.5, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), height: 50, ), @@ -204,7 +204,7 @@ class GiphyAttachment extends AttachmentWidget { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .accentBlue, + .accentPrimary, fontWeight: FontWeight.bold, ), ), @@ -227,7 +227,7 @@ class GiphyAttachment extends AttachmentWidget { StreamSvgIcon.eye( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), size: 16, ), @@ -242,7 +242,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ], @@ -307,8 +307,8 @@ class GiphyAttachment extends AttachmentWidget { final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, @@ -323,7 +323,7 @@ class GiphyAttachment extends AttachmentWidget { child: Material( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -336,13 +336,14 @@ class GiphyAttachment extends AttachmentWidget { child: Row( children: [ StreamSvgIcon.lightning( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, size: 16, ), Text( context.translations.giphyLabel.toUpperCase(), style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.white, + color: + StreamChatTheme.of(context).colorTheme.barsBg, fontWeight: FontWeight.bold, fontSize: 11, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index c677435a..20174567 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -103,8 +103,8 @@ class ImageAttachment extends AttachmentWidget { ); final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 48eea143..0c224d20 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -72,7 +72,7 @@ class AttachmentActionsModal extends StatelessWidget { context.translations.replyLabel, StreamSvgIcon.iconCurveLineLeftUp( size: 24, - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), () { Navigator.pop(context, ReturnActionType.reply); @@ -83,7 +83,7 @@ class AttachmentActionsModal extends StatelessWidget { context.translations.showInChatLabel, StreamSvgIcon.eye( size: 24, - color: theme.colorTheme.black, + color: theme.colorTheme.textHighEmphasis, ), onShowMessage, ), @@ -94,7 +94,7 @@ class AttachmentActionsModal extends StatelessWidget { : context.translations.saveImageLabel, StreamSvgIcon.iconSave( size: 24, - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), () { final attachment = message.attachments[currentIndex]; @@ -145,7 +145,7 @@ class AttachmentActionsModal extends StatelessWidget { context.translations.deleteLabel, StreamSvgIcon.delete( size: 24, - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, ), () { final channel = StreamChannel.of(context).channel; @@ -166,7 +166,7 @@ class AttachmentActionsModal extends StatelessWidget { ..maybePop(); } }, - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, ), ] .map((e) => Align( @@ -176,7 +176,7 @@ class AttachmentActionsModal extends StatelessWidget { .insertBetween( Container( height: 1, - color: theme.colorTheme.greyWhisper, + color: theme.colorTheme.borders, ), ), ), @@ -197,7 +197,7 @@ class AttachmentActionsModal extends StatelessWidget { }) => Material( key: key, - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, child: InkWell( onTap: onTap, child: Padding( @@ -242,7 +242,7 @@ class AttachmentActionsModal extends StatelessWidget { width: 182, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), child: Center( child: progress == null @@ -250,7 +250,7 @@ class AttachmentActionsModal extends StatelessWidget { height: 100, width: 100, child: StreamSvgIcon.error( - color: theme.colorTheme.greyGainsboro, + color: theme.colorTheme.disabled, ), ) : progress.toProgressIndicatorValue == 1.0 @@ -259,7 +259,7 @@ class AttachmentActionsModal extends StatelessWidget { height: 160, width: 160, child: StreamSvgIcon.check( - color: theme.colorTheme.greyGainsboro, + color: theme.colorTheme.disabled, ), ) : SizedBox( @@ -272,14 +272,14 @@ class AttachmentActionsModal extends StatelessWidget { value: progress.toProgressIndicatorValue, strokeWidth: 8, valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentBlue, + theme.colorTheme.accentPrimary, ), ), Center( child: Text( '${progress.toPercentage}%', style: theme.textTheme.headline.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index e8fe7e31..6bd3e0cc 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -44,7 +44,7 @@ class StreamBackButton extends StatelessWidget { padding: const EdgeInsets.all(14), child: StreamSvgIcon.left( size: 24, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), if (showUnreads) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart new file mode 100644 index 00000000..4ab6ba31 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -0,0 +1,202 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png) +/// +/// It shows the current [Channel] image. +/// +/// ```dart +/// class MyApp extends StatelessWidget { +/// final StreamChatClient client; +/// final Channel channel; +/// +/// MyApp(this.client, this.channel); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// debugShowCheckedModeBanner: false, +/// home: StreamChat( +/// client: client, +/// child: StreamChannel( +/// channel: channel, +/// child: Center( +/// child: ChannelImage( +/// channel: channel, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// +/// By default the widget radius size is 40x40 pixels. +/// Set the property [constraints] to set a custom dimension. +/// +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +class ChannelAvatar extends StatelessWidget { + /// Instantiate a new ChannelImage + const ChannelAvatar({ + Key? key, + this.channel, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : super(key: key); + + /// [BorderRadius] to display the widget + final BorderRadius? borderRadius; + + /// The channel to show the image of + final Channel? channel; + + /// The diameter of the image + final BoxConstraints? constraints; + + /// The function called when the image is tapped + final VoidCallback? onTap; + + /// If image is selected + final bool selected; + + /// Selection color for image + final Color? selectionColor; + + /// Thickness of selection image + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + final channel = this.channel ?? StreamChannel.of(context).channel; + + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; + + return BetterStreamBuilder>( + stream: channel.extraDataStream, + initialData: channel.extraData, + builder: (context, extraData) { + final channelImage = extraData['image']; + + if (channelImage != null) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + extraData['name']?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, + ), + ), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; + } + + final currentUser = streamChat.user!; + final otherMembers = channel.state!.members + .where((it) => it.userId != currentUser.id) + .toList(growable: false); + + // our own space, no other members + if (otherMembers.isEmpty) { + return BetterStreamBuilder( + stream: streamChat.client.state.userStream.map((it) => it!), + initialData: currentUser, + builder: (context, user) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // 1-1 Conversation + if (otherMembers.length == 1) { + final member = otherMembers.first; + return BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // Group conversation + return GroupAvatar( + members: otherMembers, + borderRadius: borderRadius ?? previewTheme?.borderRadius, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 9a8a3d1a..52861bee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -33,7 +33,7 @@ class _ChannelBottomSheetState extends State { final isOwner = userAsMember.role == 'owner'; return Material( - color: _streamChatThemeData.colorTheme.white, + color: _streamChatThemeData.colorTheme.barsBg, clipBehavior: Clip.antiAlias, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -147,7 +147,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.user( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: context.translations.viewInfoLabel, @@ -158,7 +158,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: context.translations.leaveGroupLabel, @@ -177,11 +177,11 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ), title: context.translations.deleteConversationLabel, - titleColor: _streamChatThemeData.colorTheme.accentRed, + titleColor: _streamChatThemeData.colorTheme.accentError, onTap: () async { setState(() { _showActions = false; @@ -196,7 +196,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: context.translations.cancelLabel, @@ -225,7 +225,7 @@ class _ChannelBottomSheetState extends State { question: context.translations.deleteConversationQuestion, cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ); final channel = _streamChannelState.channel; @@ -243,7 +243,7 @@ class _ChannelBottomSheetState extends State { question: context.translations.leaveConversationQuestion, cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ); if (res == true) { diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index a844af1c..5f6275a8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -99,7 +99,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; /// AppBar actions - /// By default it shows the [ChannelImage] + /// By default it shows the [ChannelAvatar] final List? actions; @override @@ -148,7 +148,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Padding( padding: const EdgeInsets.only(right: 10), child: Center( - child: ChannelImage( + child: ChannelAvatar( borderRadius: chatThemeData.channelTheme .channelHeaderTheme.avatarTheme?.borderRadius, constraints: chatThemeData.channelTheme diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart deleted file mode 100644 index d3e028ee..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/group_image.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png) -/// -/// It shows the current [Channel] image. -/// -/// ```dart -/// class MyApp extends StatelessWidget { -/// final StreamChatClient client; -/// final Channel channel; -/// -/// MyApp(this.client, this.channel); -/// -/// @override -/// Widget build(BuildContext context) { -/// return MaterialApp( -/// debugShowCheckedModeBanner: false, -/// home: StreamChat( -/// client: client, -/// child: StreamChannel( -/// channel: channel, -/// child: Center( -/// child: ChannelImage( -/// channel: channel, -/// ), -/// ), -/// ), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// The widget uses a [StreamBuilder] to render the channel information -/// image as soon as it updates. -/// -/// By default the widget radius size is 40x40 pixels. -/// Set the property [constraints] to set a custom dimension. -/// -/// The widget renders the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -class ChannelImage extends StatelessWidget { - /// Instantiate a new ChannelImage - const ChannelImage({ - Key? key, - this.channel, - this.constraints, - this.onTap, - this.borderRadius, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - }) : super(key: key); - - /// [BorderRadius] to display the widget - final BorderRadius? borderRadius; - - /// The channel to show the image of - final Channel? channel; - - /// The diameter of the image - final BoxConstraints? constraints; - - /// The function called when the image is tapped - final VoidCallback? onTap; - - /// If image is selected - final bool selected; - - /// Selection color for image - final Color? selectionColor; - - /// Thickness of selection image - final double selectionThickness; - - @override - Widget build(BuildContext context) { - final streamChat = StreamChat.of(context); - final channel = this.channel ?? StreamChannel.of(context).channel; - return BetterStreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, data) { - String? image; - final chatThemeData = StreamChatTheme.of(context); - if (data.containsKey('image') == true) { - image = data['image']; - } else if (channel.state?.members.length == 2) { - final otherMember = channel.state?.members - .firstWhere((member) => member.user?.id != streamChat.user?.id); - return BetterStreamBuilder( - stream: streamChat.client.state.usersStream - .map((users) => - users[otherMember?.userId] ?? otherMember!.user!) - .distinct(), - initialData: otherMember!.user, - builder: (context, user) => UserAvatar( - borderRadius: borderRadius ?? - chatThemeData - .channelPreviewTheme.avatarTheme?.borderRadius, - user: user ?? otherMember.user!, - constraints: constraints ?? - chatThemeData - .channelPreviewTheme.avatarTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: - selectionColor ?? chatThemeData.colorTheme.accentBlue, - selectionThickness: selectionThickness, - )); - } else { - final images = channel.state?.members - .where((member) => - member.user?.id != streamChat.user?.id && - member.user?.extraData['image'] != null) - .take(4) - // ignore: cast_nullable_to_non_nullable - .map((e) => e.user?.extraData['image'] as String) - .toList(); - return GroupImage( - images: images ?? [], - borderRadius: borderRadius ?? - chatThemeData.channelPreviewTheme.avatarTheme?.borderRadius, - constraints: constraints ?? - chatThemeData.channelPreviewTheme.avatarTheme?.constraints, - onTap: onTap, - selected: selected, - selectionColor: - selectionColor ?? chatThemeData.colorTheme.accentBlue, - selectionThickness: selectionThickness, - ); - } - - Widget child = ClipRRect( - borderRadius: borderRadius ?? - chatThemeData.channelPreviewTheme.avatarTheme?.borderRadius, - child: Container( - constraints: constraints ?? - chatThemeData.channelPreviewTheme.avatarTheme?.constraints, - decoration: BoxDecoration( - color: chatThemeData.colorTheme.accentBlue, - ), - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - if (image != null) - CachedNetworkImage( - imageUrl: image, - errorWidget: (_, __, ___) => Center( - child: Text( - data.containsKey('name') ? data['name'][0] : '', - style: TextStyle( - color: chatThemeData.colorTheme.white, - fontWeight: FontWeight.bold, - ), - ), - ), - fit: BoxFit.cover, - ) - else - chatThemeData.defaultChannelImage( - context, - channel, - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - ), - ), - ], - ), - ), - ); - if (selected) { - child = ClipRRect( - key: const Key('selectedImage'), - borderRadius: (borderRadius ?? - chatThemeData.ownMessageTheme.avatarTheme?.borderRadius ?? - BorderRadius.zero) + - BorderRadius.circular(selectionThickness), - child: Container( - constraints: constraints ?? - chatThemeData.ownMessageTheme.avatarTheme?.constraints, - color: selectionColor ?? chatThemeData.colorTheme.accentBlue, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: child, - ), - ), - ); - } - return child; - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index c4ea92c9..e9c2b7c9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -148,7 +148,7 @@ class ChannelInfo extends StatelessWidget { child: Text( context.translations.tryAgainLabel, style: textStyle?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 6c3eea20..6ef3ba10 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -154,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { Color? color; switch (status) { case ConnectionStatus.connected: - color = chatThemeData.colorTheme.accentBlue; + color = chatThemeData.colorTheme.accentPrimary; break; case ConnectionStatus.connecting: color = Colors.grey; @@ -209,7 +209,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { return Text( context.translations.streamChatLabel, style: chatThemeData.textTheme.headlineBold.copyWith( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), ); } @@ -262,7 +262,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { style: chatThemeData.channelListHeaderTheme.title?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 847a265b..5ef8d706 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -239,39 +239,32 @@ class _ChannelListViewState extends State { } Widget _buildListView(BuildContext context, List channels) { - late Widget child; - - if (channels.isNotEmpty) { - if (widget.crossAxisCount > 1) { - child = GridView.builder( - padding: widget.padding, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - ), - itemCount: channels.length, - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) => - _gridItemBuilder(context, index, channels), - ); - } else { - child = ListView.separated( - padding: widget.padding, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: - channels.isNotEmpty ? channels.length + 1 : channels.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) => - _listItemBuilder(context, index, channels), - ); - } + if (widget.crossAxisCount > 1) { + return GridView.builder( + padding: widget.padding, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + ), + itemCount: channels.length, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) => + _gridItemBuilder(context, index, channels), + ); } - - return child; + return ListView.separated( + padding: widget.padding, + physics: const AlwaysScrollableScrollPhysics(), + // all channels + progress loader + itemCount: channels.length + 1, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder!(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) => + _listItemBuilder(context, index, channels), + ); } Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( @@ -292,7 +285,7 @@ class _ChannelListViewState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon.message( size: 136, - color: chatThemeData.colorTheme.greyGainsboro, + color: chatThemeData.colorTheme.disabled, ), ), Padding( @@ -311,7 +304,7 @@ class _ChannelListViewState extends State { context.translations.sendingFirstMessageLabel, textAlign: TextAlign.center, style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), @@ -329,7 +322,7 @@ class _ChannelListViewState extends State { child: Text( context.translations.startAChatLabel, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), @@ -364,8 +357,8 @@ class _ChannelListViewState extends State { final chatThemeData = StreamChatTheme.of(context); if (widget.crossAxisCount > 1) { return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, + baseColor: chatThemeData.colorTheme.disabled, + highlightColor: chatThemeData.colorTheme.inputBg, child: Column( children: [ const SizedBox(height: 4), @@ -393,12 +386,12 @@ class _ChannelListViewState extends State { ); } else { return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, + baseColor: chatThemeData.colorTheme.disabled, + highlightColor: chatThemeData.colorTheme.inputBg, child: ListTile( leading: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, shape: BoxShape.circle, ), constraints: const BoxConstraints.tightFor( @@ -414,7 +407,7 @@ class _ChannelListViewState extends State { alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.tightFor( @@ -431,7 +424,7 @@ class _ChannelListViewState extends State { alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.expand( @@ -443,7 +436,7 @@ class _ChannelListViewState extends State { Container( margin: const EdgeInsets.only(left: 16), decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.tightFor( @@ -488,70 +481,73 @@ class _ChannelListViewState extends State { Widget _listItemBuilder(BuildContext context, int i, List channels) { final channelsBloc = ChannelsBloc.of(context); + + if (i == channels.length) { + return _buildQueryProgressIndicator(context, channelsBloc); + } + final onTap = _getChannelTap(context); final chatThemeData = StreamChatTheme.of(context); - final backgroundColor = chatThemeData.colorTheme.whiteSmoke; + final backgroundColor = chatThemeData.colorTheme.inputBg; + final channel = channels[i]; - if (i < channels.length) { - final channel = channels[i]; - - return StreamChannel( - key: ValueKey('CHANNEL-${channel.cid}'), - channel: channel, - child: Slidable( - controller: _slideController, - enabled: widget.swipeToAction, - actionPane: const SlidableBehindActionPane(), - actionExtentRatio: 0.12, - secondaryActions: widget.swipeActions - ?.map((e) => IconSlideAction( - color: e.color, - iconWidget: e.iconWidget, - onTap: () { - e.onTap?.call(channel); - }, - )) - .toList() ?? - [ + return StreamChannel( + key: ValueKey('CHANNEL-${channel.cid}'), + channel: channel, + child: Slidable( + controller: _slideController, + enabled: widget.swipeToAction, + actionPane: const SlidableBehindActionPane(), + actionExtentRatio: 0.12, + secondaryActions: widget.swipeActions + ?.map((e) => IconSlideAction( + color: e.color, + iconWidget: e.iconWidget, + onTap: () { + e.onTap?.call(channel); + }, + )) + .toList() ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed != null + ? () { + widget.onMoreDetailsPressed!(channel); + } + : () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), + ), + ); + }, + ), + if ([ + 'admin', + 'owner', + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user?.id) + ?.role)) IconSlideAction( color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed != null - ? () { - widget.onMoreDetailsPressed!(channel); - } - : () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), - ), - ); - }, - ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, + iconWidget: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentError, ), onTap: widget.onDeletePressed != null ? () { @@ -563,36 +559,33 @@ class _ChannelListViewState extends State { title: context.translations.deleteConversationLabel, question: context - .translations.deleteConversationQuestion, + .translations.deleteConversationQuestion, okText: context.translations.deleteLabel, - cancelText: context.translations.cancelLabel, - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], - child: DecoratedBox( - decoration: BoxDecoration( - color: chatThemeData.colorTheme.whiteSnow, - ), - child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), + cancelText: context.translations.cancelLabel, + icon: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentError, + ), + ); + if (res == true) { + await channel.delete(); + } + }, ), + ], + child: DecoratedBox( + decoration: BoxDecoration( + color: chatThemeData.colorTheme.appBg, ), + child: widget.channelPreviewBuilder?.call(context, channel) ?? + ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), - ); - } else { - return _buildQueryProgressIndicator(context, channelsBloc); - } + ), + ); } ChannelTapCallback _getChannelTap(BuildContext context) { @@ -628,7 +621,7 @@ class _ChannelListViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ChannelImage( + ChannelAvatar( channel: channel, borderRadius: BorderRadius.circular(32), selected: selected, @@ -661,28 +654,33 @@ class _ChannelListViewState extends State { ChannelsBlocState channelsProvider, ) => BetterStreamBuilder( - stream: channelsProvider.queryChannelsLoading, - initialData: false, - errorBuilder: (context, err) => Container( - color: StreamChatTheme.of(context) - .colorTheme - .accentRed - .withOpacity(.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Text(context.translations.loadingChannelsError), - ), + stream: channelsProvider.queryChannelsLoading, + initialData: false, + errorBuilder: (context, err) { + final theme = StreamChatTheme.of(context); + return Container( + color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.translations.loadingChannelsError, + style: theme.textTheme.body.copyWith( + color: Colors.white, ), ), - builder: (context, data) => data - ? const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ) - : const Offstage()); + ), + ); + }, + builder: (context, showLoading) { + if (!showLoading) return const Offstage(); + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ); + }, + ); Widget _separatorBuilder(context, i) { final effect = StreamChatTheme.of(context).colorTheme.borderBottom; diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index f21625c6..432f3354 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -56,7 +56,7 @@ class ChannelPreview extends StatelessWidget { final Widget? subtitle; /// Widget rendering the leading element, by default - /// it shows the [ChannelImage] + /// it shows the [ChannelAvatar] final Widget? leading; /// Widget rendering the trailing element, @@ -82,20 +82,9 @@ class ChannelPreview extends StatelessWidget { contentPadding: const EdgeInsets.symmetric( horizontal: 8, ), - onTap: () { - if (onTap != null) { - onTap!(channel); - } - }, - onLongPress: () { - if (onLongPress != null) { - onLongPress!(channel); - } - }, - leading: leading ?? - ChannelImage( - onTap: onImageTap, - ), + onTap: () => onTap?.call(channel), + onLongPress: () => onLongPress?.call(channel), + leading: leading ?? ChannelAvatar(onTap: onImageTap), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index e23f0233..3d85299c 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -56,7 +56,7 @@ class DateDivider extends StatelessWidget { child: Text( dayInfo, style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index e62a7cfa..0a6c337a 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -40,8 +40,8 @@ class DeletedMessage extends StatelessWidget { side: borderSide ?? BorderSide( color: Theme.of(context).brightness == Brightness.dark - ? chatThemeData.colorTheme.white.withAlpha(24) - : chatThemeData.colorTheme.black.withAlpha(24), + ? chatThemeData.colorTheme.barsBg.withAlpha(24) + : chatThemeData.colorTheme.textHighEmphasis.withAlpha(24), ), ), child: Padding( diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 6810816f..287612ad 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -6,8 +6,8 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:stream_chat_flutter/src/image_footer.dart'; -import 'package:stream_chat_flutter/src/image_header.dart'; +import 'package:stream_chat_flutter/src/gallery_footer.dart'; +import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -181,7 +181,7 @@ class _FullScreenMediaState extends State child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ImageHeader( + GalleryHeader( userName: widget.userName, // TODO: Fix this sentAt: context.translations.sentAtText( @@ -201,7 +201,7 @@ class _FullScreenMediaState extends State }, ), if (widget.message.type != 'ephemeral') - ImageFooter( + GalleryFooter( currentPage: _currentPage, totalPages: widget.mediaAttachments.length, mediaAttachments: widget.mediaAttachments, diff --git a/packages/stream_chat_flutter/lib/src/image_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart similarity index 90% rename from packages/stream_chat_flutter/lib/src/image_footer.dart rename to packages/stream_chat_flutter/lib/src/gallery_footer.dart index 3d12bf94..ed8c39f9 100644 --- a/packages/stream_chat_flutter/lib/src/image_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -14,9 +14,9 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; /// Footer widget for media display -class ImageFooter extends StatefulWidget implements PreferredSizeWidget { +class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Creates a channel header - const ImageFooter({ + const GalleryFooter({ Key? key, required this.message, this.onBackPressed, @@ -55,13 +55,13 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget { final ValueChanged? mediaSelectedCallBack; @override - _ImageFooterState createState() => _ImageFooterState(); + _GalleryFooterState createState() => _GalleryFooterState(); @override final Size preferredSize; } -class _ImageFooterState extends State { +class _GalleryFooterState extends State { final TextEditingController _messageController = TextEditingController(); final FocusNode _messageFocusNode = FocusNode(); @@ -79,7 +79,7 @@ class _ImageFooterState extends State { Widget build(BuildContext context) { const showShareButton = !kIsWeb; final mediaQueryData = MediaQuery.of(context); - final chatThemeData = StreamChatTheme.of(context); + final galleryFooterThemeData = GalleryFooterTheme.of(context); return SizedBox.fromSize( size: Size( mediaQueryData.size.width, @@ -89,7 +89,7 @@ class _ImageFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: chatThemeData.colorTheme.white, + color: galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -101,7 +101,7 @@ class _ImageFooterState extends State { IconButton( icon: StreamSvgIcon.iconShare( size: 24, - color: chatThemeData.colorTheme.black, + color: galleryFooterThemeData.shareIconColor, ), onPressed: () async { final attachment = @@ -137,7 +137,7 @@ class _ImageFooterState extends State { children: [ Text( '${widget.currentPage + 1} of ${widget.totalPages}', - style: chatThemeData.textTheme.headlineBold, + style: galleryFooterThemeData.titleTextStyle, ), ], ), @@ -145,7 +145,7 @@ class _ImageFooterState extends State { ), IconButton( icon: StreamSvgIcon.iconGrid( - color: chatThemeData.colorTheme.black, + color: galleryFooterThemeData.gridIconButtonColor, ), onPressed: () => _showPhotosModal(context), ), @@ -158,10 +158,11 @@ class _ImageFooterState extends State { void _showPhotosModal(context) { final chatThemeData = StreamChatTheme.of(context); + final galleryFooterThemeData = GalleryFooterTheme.of(context); showModalBottomSheet( context: context, - barrierColor: chatThemeData.colorTheme.overlay, - backgroundColor: chatThemeData.colorTheme.white, + barrierColor: galleryFooterThemeData.bottomSheetBarrierColor, + backgroundColor: galleryFooterThemeData.bottomSheetBackgroundColor, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -192,7 +193,8 @@ class _ImageFooterState extends State { padding: const EdgeInsets.all(16), child: Text( context.translations.photosLabel, - style: chatThemeData.textTheme.headlineBold, + style: + galleryFooterThemeData.bottomSheetPhotosTextStyle, ), ), ), @@ -200,7 +202,8 @@ class _ImageFooterState extends State { alignment: Alignment.centerRight, child: IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.black, + color: + galleryFooterThemeData.bottomSheetCloseIconColor, ), onPressed: () => Navigator.maybePop(context), ), @@ -263,7 +266,8 @@ class _ImageFooterState extends State { boxShadow: [ BoxShadow( blurRadius: 8, - color: chatThemeData.colorTheme.black + color: chatThemeData + .colorTheme.textHighEmphasis .withOpacity(0.3), ), ], diff --git a/packages/stream_chat_flutter/lib/src/image_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart similarity index 83% rename from packages/stream_chat_flutter/lib/src/image_header.dart rename to packages/stream_chat_flutter/lib/src/gallery_header.dart index 3d156b16..6c4ecfee 100644 --- a/packages/stream_chat_flutter/lib/src/image_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -5,9 +5,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Header/AppBar widget for media display screen -class ImageHeader extends StatelessWidget implements PreferredSizeWidget { +class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Creates a channel header - const ImageHeader({ + const GalleryHeader({ Key? key, required this.message, this.currentIndex = 0, @@ -51,7 +51,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final galleryHeaderThemeData = GalleryHeaderTheme.of(context); return AppBar( textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, @@ -59,18 +59,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { leading: showBackButton ? IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.black, + color: galleryHeaderThemeData.closeButtonColor, size: 24, ), onPressed: onBackPressed, ) : const SizedBox(), - backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: galleryHeaderThemeData.backgroundColor, actions: [ if (message.type != 'ephemeral') IconButton( icon: StreamSvgIcon.iconMenuPoint( - color: chatThemeData.colorTheme.black, + color: galleryHeaderThemeData.iconMenuPointColor, ), onPressed: () { _showMessageActionModalBottomSheet(context); @@ -90,11 +90,11 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( userName, - style: chatThemeData.textTheme.headlineBold, + style: galleryHeaderThemeData.titleTextStyle, ), Text( sentAt, - style: chatThemeData.channelPreviewTheme.subtitle, + style: galleryHeaderThemeData.subtitleTextStyle, ), ], ), @@ -109,11 +109,13 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { void _showMessageActionModalBottomSheet(BuildContext context) async { final channel = StreamChannel.of(context).channel; + final galleryHeaderThemeData = + StreamChatTheme.of(context).galleryHeaderTheme; final result = await showDialog( useRootNavigator: false, context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor, builder: (context) => StreamChannel( channel: channel, child: AttachmentActionsModal( diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart new file mode 100644 index 00000000..4a16279e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Widget for constructing a group of images +class GroupAvatar extends StatelessWidget { + /// Constructor for creating a [GroupAvatar] + const GroupAvatar({ + Key? key, + required this.members, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : super(key: key); + + /// List of images to display + final List members; + + /// Constraints on the widget + final BoxConstraints? constraints; + + /// Callback when widget is tapped + final VoidCallback? onTap; + + /// Highlights if selected + final bool selected; + + /// [BorderRadius] to pass to the widget + final BorderRadius? borderRadius; + + /// Color of selection if selected + final Color? selectionColor; + + /// Thickness with which color of selection is shown + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; + + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + final streamChatTheme = StreamChatTheme.of(context); + final colorTheme = streamChatTheme.colorTheme; + final previewTheme = streamChatTheme.channelPreviewTheme.avatarTheme; + + Widget avatar = GestureDetector( + onTap: onTap, + child: ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: Flex( + direction: Axis.vertical, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + fit: FlexFit.tight, + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: members + .take(2) + .map( + (member) => Flexible( + fit: FlexFit.tight, + child: FittedBox( + fit: BoxFit.cover, + clipBehavior: Clip.antiAlias, + child: Transform.scale( + scale: 1.2, + child: BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => UserAvatar( + user: member.user!, + borderRadius: BorderRadius.zero, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + if (members.length > 2) + Flexible( + fit: FlexFit.tight, + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: members + .skip(2) + .take(2) + .map( + (member) => Flexible( + fit: FlexFit.tight, + child: FittedBox( + fit: BoxFit.cover, + clipBehavior: Clip.antiAlias, + child: Transform.scale( + scale: 1.2, + child: BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => UserAvatar( + user: member.user!, + borderRadius: BorderRadius.zero, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + ], + ), + ), + ), + ); + + if (selected) { + avatar = ClipRRect( + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? previewTheme?.borderRadius ?? BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: avatar, + ), + ), + ); + } + + return avatar; + } +} diff --git a/packages/stream_chat_flutter/lib/src/group_image.dart b/packages/stream_chat_flutter/lib/src/group_image.dart deleted file mode 100644 index c6778a91..00000000 --- a/packages/stream_chat_flutter/lib/src/group_image.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Widget for constructing a group of images -class GroupImage extends StatelessWidget { - /// Constructor for creating a [GroupImage] - const GroupImage({ - Key? key, - required this.images, - this.constraints, - this.onTap, - this.borderRadius, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - }) : super(key: key); - - /// List of images to display - final List images; - - /// Constraints on the widget - final BoxConstraints? constraints; - - /// Callback when widget is tapped - final VoidCallback? onTap; - - /// Highlights if selected - final bool selected; - - /// [BorderRadius] to pass to the widget - final BorderRadius? borderRadius; - - /// Color of selection if selected - final Color? selectionColor; - - /// Thickness with which color of selection is shown - final double selectionThickness; - - @override - Widget build(BuildContext context) { - Widget? avatar; - final streamChatTheme = StreamChatTheme.of(context); - - avatar = GestureDetector( - onTap: onTap, - child: ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - child: Container( - constraints: constraints ?? - streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentBlue, - ), - child: Flex( - direction: Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - fit: FlexFit.tight, - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: images - .take(2) - .map((url) => Flexible( - fit: FlexFit.tight, - child: FittedBox( - fit: BoxFit.cover, - clipBehavior: Clip.antiAlias, - child: Transform.scale( - scale: 1.2, - child: CachedNetworkImage( - imageUrl: url, - fit: BoxFit.cover, - ), - ), - ), - )) - .toList(), - ), - ), - if (images.length > 2) - Flexible( - fit: FlexFit.tight, - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: images - .skip(2) - .map((url) => Flexible( - fit: FlexFit.tight, - child: FittedBox( - fit: BoxFit.cover, - clipBehavior: Clip.antiAlias, - child: Transform.scale( - scale: 1.2, - child: CachedNetworkImage( - imageUrl: url, - fit: BoxFit.cover, - ), - ), - ), - )) - .toList(), - ), - ), - ], - ), - ), - ), - ); - - if (selected) { - avatar = ClipRRect( - borderRadius: (borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius ?? - BorderRadius.zero) + - BorderRadius.circular(selectionThickness), - child: Container( - color: selectionColor ?? streamChatTheme.colorTheme.accentBlue, - height: 64, - width: 64, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: avatar, - ), - ), - ); - } - - return avatar; - } -} diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 1e935942..965d4fed 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -46,8 +46,8 @@ class InfoTile extends StatelessWidget { childAnchor: childAnchor ?? Alignment.bottomCenter, portal: Container( height: 25, - color: - backgroundColor ?? chatThemeData.colorTheme.grey.withOpacity(0.9), + color: backgroundColor ?? + chatThemeData.colorTheme.textLowEmphasis.withOpacity(0.9), child: Center( child: Text( message, diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index fe0070f9..77359f8b 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -90,8 +90,8 @@ class _MediaListViewState extends State { ? 1.0 : 0.0, child: Container( - color: - chatThemeData.colorTheme.black.withOpacity(0.5), + color: chatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5), alignment: Alignment.topRight, padding: const EdgeInsets.only( top: 8, @@ -99,10 +99,11 @@ class _MediaListViewState extends State { ), child: CircleAvatar( radius: 12, - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: chatThemeData.colorTheme.barsBg, child: StreamSvgIcon.check( size: 24, - color: chatThemeData.colorTheme.black, + color: + chatThemeData.colorTheme.textHighEmphasis, ), ), ), @@ -124,7 +125,7 @@ class _MediaListViewState extends State { child: Text( media.videoDuration.format(), style: TextStyle( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart index 99b4b962..d9469215 100644 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/mention_tile.dart @@ -76,7 +76,7 @@ class MentionTile extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: chatThemeData.textTheme.footnoteBold.copyWith( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ], @@ -90,7 +90,7 @@ class MentionTile extends StatelessWidget { left: 8, ), child: StreamSvgIcon.mentions( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index b7ba4655..2f37d637 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -165,7 +165,7 @@ class _MessageActionsModalState extends State { child: SizedBox( width: mediaQueryData.size.width * 0.75, child: Material( - color: streamChatThemeData.colorTheme.whiteSnow, + color: streamChatThemeData.colorTheme.appBg, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), @@ -197,7 +197,7 @@ class _MessageActionsModalState extends State { ].insertBetween( Container( height: 1, - color: streamChatThemeData.colorTheme.greyWhisper, + color: streamChatThemeData.colorTheme.borders, ), ), ), @@ -270,7 +270,7 @@ class _MessageActionsModalState extends State { context, title: context.translations.flagMessageLabel, icon: StreamSvgIcon.flag( - color: streamChatThemeData.colorTheme.accentRed, + color: streamChatThemeData.colorTheme.accentError, size: 24, ), question: context.translations.flagMessageQuestion, @@ -285,7 +285,7 @@ class _MessageActionsModalState extends State { await showInfoDialog( context, icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, size: 24, ), details: context.translations.flagMessageSuccessfulText, @@ -298,7 +298,7 @@ class _MessageActionsModalState extends State { await showInfoDialog( context, icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, size: 24, ), details: context.translations.flagMessageSuccessfulText, @@ -335,7 +335,7 @@ class _MessageActionsModalState extends State { context, title: context.translations.deleteMessageLabel, icon: StreamSvgIcon.flag( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), question: context.translations.deleteMessageQuestion, @@ -361,7 +361,7 @@ class _MessageActionsModalState extends State { showInfoDialog( context, icon: StreamSvgIcon.error( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), details: context.translations.operationCouldNotBeCompletedText, @@ -542,7 +542,7 @@ class _MessageActionsModalState extends State { child: Row( children: [ StreamSvgIcon.circleUp( - color: streamChatThemeData.colorTheme.accentBlue, + color: streamChatThemeData.colorTheme.accentPrimary, ), const SizedBox(width: 16), Text( @@ -589,7 +589,7 @@ class _MessageActionsModalState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.edit( - color: streamChatThemeData.colorTheme.greyGainsboro, + color: streamChatThemeData.colorTheme.disabled, ), ), Text( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 8bea5313..702cfe4e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -26,6 +26,18 @@ import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +export 'package:video_compress/video_compress.dart' show VideoQuality; + +/// A callback that can be passed to [MessageInput.onError]. +/// +/// This callback should not throw. +/// +/// It exists merely for error reporting, and should not be used otherwise. +typedef ErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + /// Builder for attachment thumbnails typedef AttachmentThumbnailBuilder = Widget Function( BuildContext, @@ -150,11 +162,20 @@ class MessageInput extends StatefulWidget { this.showCommandsButton = true, this.mentionsTileBuilder, this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.compressedVideoQuality = VideoQuality.DefaultQuality, + this.compressedVideoFrameRate = 30, + this.onError, }) : super(key: key); /// Message to edit final Message? editMessage; + /// Video quality to use when compressing the videos + final VideoQuality compressedVideoQuality; + + /// Frame rate to use when compressing the videos + final int compressedVideoFrameRate; + /// Max attachment size in bytes /// Defaults to 20 MB /// do not set it if you're using our default CDN @@ -224,6 +245,9 @@ class MessageInput extends StatefulWidget { /// Customize the tile for the mentions overlay final MentionTileBuilder? mentionsTileBuilder; + /// A callback for error reporting + final ErrorListener? onError; + @override MessageInputState createState() => MessageInputState(); @@ -331,7 +355,7 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.greyGainsboro, + color: _streamChatTheme.colorTheme.disabled, ), ), const Text( @@ -398,7 +422,8 @@ class MessageInputState extends State { border: _sendAsDm ? null : Border.all( - color: _streamChatTheme.colorTheme.black.withOpacity(.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), width: 2, ), borderRadius: BorderRadius.circular(3), @@ -407,8 +432,8 @@ class MessageInputState extends State { child: Material( borderRadius: BorderRadius.circular(3), color: _sendAsDm - ? _streamChatTheme.colorTheme.accentBlue - : _streamChatTheme.colorTheme.white, + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { setState(() { @@ -423,7 +448,7 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: StreamSvgIcon.check( size: 16, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), secondChild: const SizedBox( height: 16, @@ -439,7 +464,8 @@ class MessageInputState extends State { child: Text( context.translations.alsoSendAsDirectMessageLabel, style: _streamChatTheme.textTheme.footnote.copyWith( - color: _streamChatTheme.colorTheme.black.withOpacity(0.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), ), ), ), @@ -579,7 +605,7 @@ class MessageInputState extends State { isDense: true, hintText: _getHint(context), hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( - color: _streamChatTheme.colorTheme.grey, + color: _streamChatTheme.colorTheme.textLowEmphasis, ), border: const OutlineInputBorder( borderSide: BorderSide( @@ -617,7 +643,7 @@ class MessageInputState extends State { constraints: BoxConstraints.tight(const Size(64, 24)), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), alignment: Alignment.center, child: Row( @@ -831,12 +857,12 @@ class MessageInputState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, clipBehavior: Clip.hardEdge, child: Container( constraints: BoxConstraints.loose(const Size.fromHeight(400)), decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: BorderRadius.circular(8)), child: ListView( padding: const EdgeInsets.all(0), @@ -852,14 +878,14 @@ class MessageInputState extends State { horizontal: 8, ), child: StreamSvgIcon.lightning( - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), ), Text( context.translations.instantCommandsLabel, style: TextStyle( - color: - _streamChatTheme.colorTheme.black.withOpacity(.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), ), ) ], @@ -899,7 +925,7 @@ class MessageInputState extends State { color: _streamChatTheme // ignore: lines_longer_than_80_chars .colorTheme - .grey, + .textLowEmphasis, ), ), ], @@ -943,24 +969,29 @@ class MessageInputState extends State { switch (index) { case 0: return _attachments.isEmpty - ? streamChatThemeData.colorTheme.accentBlue + ? streamChatThemeData.colorTheme.accentPrimary : (!_attachmentContainsFile - ? streamChatThemeData.colorTheme.accentBlue - : streamChatThemeData.colorTheme.black.withOpacity(0.2)); + ? streamChatThemeData.colorTheme.accentPrimary + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); case 1: return _attachmentContainsFile - ? streamChatThemeData.colorTheme.accentBlue + ? streamChatThemeData.colorTheme.accentPrimary : (_attachments.isEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.5) - : streamChatThemeData.colorTheme.black.withOpacity(0.2)); + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); case 2: return _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.2) - : streamChatThemeData.colorTheme.black.withOpacity(0.5); + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); case 3: return _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.2) - : streamChatThemeData.colorTheme.black.withOpacity(0.5); + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); default: return Colors.black; } @@ -970,7 +1001,7 @@ class MessageInputState extends State { duration: const Duration(milliseconds: 300), height: _openFilePickerSection ? _filePickerSize : 0, child: Material( - color: _streamChatTheme.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.inputBg, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1033,7 +1064,7 @@ class MessageInputState extends State { }, child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -1049,7 +1080,7 @@ class MessageInputState extends State { height: 4, child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.inputBg, borderRadius: BorderRadius.circular(4), ), ), @@ -1063,7 +1094,7 @@ class MessageInputState extends State { Expanded( child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( @@ -1106,8 +1137,11 @@ class MessageInputState extends State { if (file.size! > widget.maxAttachmentSize) { if (medium.type == AssetType.video && file.path != null) { - final mediaInfo = await (VideoService.compressVideo(file.path!) - as FutureOr); + final mediaInfo = await (VideoService.compressVideo( + file.path!, + frameRate: widget.compressedVideoFrameRate, + quality: widget.compressedVideoQuality, + ) as FutureOr); if (mediaInfo.filesize! > widget.maxAttachmentSize) { _showErrorAlert( @@ -1147,7 +1181,7 @@ class MessageInputState extends State { ); case 'ban': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.iconUserDelete( size: 16, @@ -1156,7 +1190,7 @@ class MessageInputState extends State { ); case 'flag': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.flag( size: 14, @@ -1165,7 +1199,7 @@ class MessageInputState extends State { ); case 'imgur': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: ClipOval( child: StreamSvgIcon.imgur( @@ -1175,7 +1209,7 @@ class MessageInputState extends State { ); case 'mute': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.mute( size: 16, @@ -1184,7 +1218,7 @@ class MessageInputState extends State { ); case 'unban': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.userAdd( size: 16, @@ -1193,7 +1227,7 @@ class MessageInputState extends State { ); case 'unmute': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.volumeUp( size: 16, @@ -1202,7 +1236,7 @@ class MessageInputState extends State { ); default: return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.lightning( size: 16, @@ -1242,7 +1276,7 @@ class MessageInputState extends State { final child = Card( margin: const EdgeInsets.all(8), elevation: 2, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1250,7 +1284,7 @@ class MessageInputState extends State { child: Container( constraints: BoxConstraints.loose(const Size.fromHeight(240)), decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), child: FutureBuilder>( future: queryMembers ?? Future.value(members), @@ -1266,7 +1300,7 @@ class MessageInputState extends State { .where((it) => it.user != null) .map( (m) => Material( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { if (m.user != null) { @@ -1353,7 +1387,7 @@ class MessageInputState extends State { child: Card( margin: const EdgeInsets.all(8), elevation: 2, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1368,7 +1402,7 @@ class MessageInputState extends State { offset: Offset(0, -4), ), ], - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), child: ListView.builder( padding: const EdgeInsets.all(0), @@ -1384,8 +1418,8 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: StreamSvgIcon.smile( - color: - _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme + .colorTheme.accentPrimary, ), ), Flexible( @@ -1394,7 +1428,8 @@ class MessageInputState extends State { query, ), style: TextStyle( - color: _streamChatTheme.colorTheme.black + color: _streamChatTheme + .colorTheme.textHighEmphasis .withOpacity(.5), ), ), @@ -1566,11 +1601,12 @@ class MessageInputState extends State { onPressed: () { setState(() => _attachments.remove(attachment.id)); }, - fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5), + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.5), child: Center( child: StreamSvgIcon.close( size: 24, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), ), ), @@ -1605,8 +1641,8 @@ class MessageInputState extends State { errorWidget: (_, obj, trace) => getFileTypeImage(attachment.extraData['other'] as String?), placeholder: (context, _) => Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.greyGainsboro, - highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, child: Image.asset( 'images/placeholder.png', fit: BoxFit.cover, @@ -1647,7 +1683,7 @@ class MessageInputState extends State { return IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty - ? _streamChatTheme.colorTheme.greyGainsboro + ? _streamChatTheme.colorTheme.disabled : (_commandsOverlay != null ? _streamChatTheme.messageInputTheme.actionButtonColor : _streamChatTheme.messageInputTheme.actionButtonIdleColor), @@ -1823,11 +1859,11 @@ class MessageInputState extends State { } if (camera) { - PickedFile? pickedFile; + XFile? pickedFile; if (fileType == DefaultAttachmentTypes.image) { - pickedFile = await _imagePicker.getImage(source: ImageSource.camera); + pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); } else if (fileType == DefaultAttachmentTypes.video) { - pickedFile = await _imagePicker.getVideo(source: ImageSource.camera); + pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); } if (pickedFile == null) { return; @@ -1879,8 +1915,11 @@ class MessageInputState extends State { if (file.size! > widget.maxAttachmentSize) { if (attachmentType == 'video' && file.path != null) { - final mediaInfo = await (VideoService.compressVideo(file.path!) - as FutureOr); + final mediaInfo = await (VideoService.compressVideo( + file.path!, + frameRate: widget.compressedVideoFrameRate, + quality: widget.compressedVideoQuality, + ) as FutureOr); if (mediaInfo.filesize! > widget.maxAttachmentSize) { _showErrorAlert( @@ -1957,7 +1996,7 @@ class MessageInputState extends State { } /// Sends the current message - void sendMessage() async { + Future sendMessage() async { var text = textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; @@ -1973,9 +2012,7 @@ class MessageInputState extends State { textEditingController.clear(); _attachments.clear(); - if (widget.onQuotedMessageCleared != null) { - widget.onQuotedMessageCleared!(); - } + widget.onQuotedMessageCleared?.call(); setState(() { _messageIsPresent = false; @@ -1987,7 +2024,6 @@ class MessageInputState extends State { _mentionsOverlay?.remove(); _mentionsOverlay = null; - Future sendingFuture; Message message; if (widget.editMessage != null) { message = widget.editMessage!.copyWith( @@ -2025,33 +2061,39 @@ class MessageInputState extends State { _mentionedUsers.clear(); - if (widget.editMessage == null || - widget.editMessage!.status == MessageSendingStatus.failed || - widget.editMessage!.status == MessageSendingStatus.sending) { - sendingFuture = channel.sendMessage(message); - } else { - sendingFuture = channel.updateMessage(message); - } + try { + Future sendingFuture; + if (widget.editMessage == null || + widget.editMessage!.status == MessageSendingStatus.failed || + widget.editMessage!.status == MessageSendingStatus.sending) { + sendingFuture = channel.sendMessage(message); + } else { + sendingFuture = channel.updateMessage(message); + } - if (!shouldUnfocus) { - FocusScope.of(context).requestFocus(_focusNode); - } + if (!shouldUnfocus) { + FocusScope.of(context).requestFocus(_focusNode); + } - return sendingFuture.then((resp) { + final resp = await sendingFuture; if (resp.message?.type == 'error') { _parseExistingMessage(message); } - if (widget.onMessageSent != null) { - widget.onMessageSent!(resp.message); + widget.onMessageSent?.call(resp.message); + } catch (e, stk) { + if (widget.onError != null) { + widget.onError?.call(e, stk); + } else { + rethrow; } - }); + } } StreamSubscription? _keyboardListener; void _showErrorAlert(String description) { showModalBottomSheet( - backgroundColor: _streamChatTheme.colorTheme.white, + backgroundColor: _streamChatTheme.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -2065,7 +2107,7 @@ class MessageInputState extends State { height: 26, ), StreamSvgIcon.error( - color: _streamChatTheme.colorTheme.accentRed, + color: _streamChatTheme.colorTheme.accentError, size: 24, ), const SizedBox( @@ -2089,7 +2131,8 @@ class MessageInputState extends State { height: 36, ), Container( - color: _streamChatTheme.colorTheme.black.withOpacity(.08), + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.08), height: 1, ), Row( @@ -2101,8 +2144,8 @@ class MessageInputState extends State { }, child: Text( context.translations.okLabel, - style: _streamChatTheme.textTheme.bodyBold - .copyWith(color: _streamChatTheme.colorTheme.accentBlue), + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary), ), ), ], @@ -2243,12 +2286,12 @@ class __PickerWidgetState extends State<_PickerWidget> { }, child: Container( constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.inputBg, alignment: Alignment.center, child: Text( context.translations.addMoreFilesLabel, style: TextStyle( - color: widget.streamChatTheme.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), ), @@ -2266,7 +2309,7 @@ class __PickerWidgetState extends State<_PickerWidget> { PhotoManager.openSetting(); }, child: Container( - color: widget.streamChatTheme.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.inputBg, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -2275,12 +2318,13 @@ class __PickerWidgetState extends State<_PickerWidget> { 'svgs/icon_picture_empty_state.svg', package: 'stream_chat_flutter', height: 140, - color: widget.streamChatTheme.colorTheme.greyGainsboro, + color: widget.streamChatTheme.colorTheme.disabled, ), Text( context.translations.enablePhotoAndVideoAccessMessage, style: widget.streamChatTheme.textTheme.body.copyWith( - color: widget.streamChatTheme.colorTheme.grey), + color: + widget.streamChatTheme.colorTheme.textLowEmphasis), textAlign: TextAlign.center, ), const SizedBox(height: 6), @@ -2288,7 +2332,7 @@ class __PickerWidgetState extends State<_PickerWidget> { child: Text( context.translations.allowGalleryAccessMessage, style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 72a2a314..2c250eaa 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -143,7 +143,6 @@ class MessageListView extends StatefulWidget { this.parentMessage, this.threadBuilder, this.onThreadTap, - this.onReplyTap, this.dateDividerBuilder, this.scrollPhysics = const ClampingScrollPhysics(), this.initialScrollIndex, @@ -153,7 +152,6 @@ class MessageListView extends StatefulWidget { this.onMessageSwiped, this.highlightInitialMessage = false, this.messageHighlightColor, - this.onShowMessage, this.showConnectionStateTile = false, this.headerBuilder, this.footerBuilder, @@ -161,16 +159,11 @@ class MessageListView extends StatefulWidget { this.emptyBuilder, this.systemMessageBuilder, this.messageListBuilder, - this.errorWidgetBuilder, + this.errorBuilder, this.messageFilter, - this.customAttachmentBuilders, this.onMessageTap, this.onSystemMessageTap, - this.onAttachmentTap, - this.onLinkTap, this.pinPermissions = const [], - this.textBuilder, - this.usernameBuilder, this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, @@ -223,9 +216,6 @@ class MessageListView extends StatefulWidget { /// Called when message item gets swiped final OnMessageSwiped? onMessageSwiped; - /// - final ReplyTapCallback? onReplyTap; - /// If true the list will highlight the initialMessage if there is any. /// /// Also See [StreamChannel] @@ -234,9 +224,6 @@ class MessageListView extends StatefulWidget { /// Color used while highlighting initial message final Color? messageHighlightColor; - /// Callback when show message is tapped - final ShowMessageCallback? onShowMessage; - /// Flag for showing tile on header final bool showConnectionStateTile; @@ -263,16 +250,11 @@ class MessageListView extends StatefulWidget { /// This parameter can be used to display an error message to /// users in the event /// of a connection failure. - final ErrorBuilder? errorWidgetBuilder; + final ErrorBuilder? errorBuilder; /// Predicate used to filter messages final bool Function(Message)? messageFilter; - /// Attachment builders for the default message widget - /// Please change this in the [MessageWidget] if you are using a - /// custom implementation - final Map? customAttachmentBuilders; - /// Called when any message is tapped except a system message /// (use [onSystemMessageTap] instead) final OnMessageTap? onMessageTap; @@ -280,18 +262,6 @@ class MessageListView extends StatefulWidget { /// Called when system message is tapped final OnMessageTap? onSystemMessageTap; - /// Customize onTap on attachment - final void Function(Message message, Attachment attachment)? onAttachmentTap; - - /// Customize the MessageWidget textBuilder - final Widget Function(BuildContext context, Message message)? textBuilder; - - /// Customize the MessageWidget usernameBuilder - final Widget Function(BuildContext context, Message message)? usernameBuilder; - - /// Callback for when link is tapped - final void Function(String link)? onLinkTap; - /// A List of user types that have permission to pin messages final List pinPermissions; @@ -370,19 +340,21 @@ class _MessageListViewState extends State { child: Text( context.translations.emptyChatMessagesText, style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.black.withOpacity(.5)), + color: _streamTheme.colorTheme.textHighEmphasis + .withOpacity(.5)), ), ), messageListBuilder: widget.messageListBuilder ?? (context, list) => _buildListView(list), messageListController: _messageListController, parentMessage: widget.parentMessage, - errorWidgetBuilder: widget.errorWidgetBuilder ?? + errorBuilder: widget.errorBuilder ?? (BuildContext context, Object error) => Center( child: Text( context.translations.genericErrorText, style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.black.withOpacity(.5)), + color: _streamTheme.colorTheme.textHighEmphasis + .withOpacity(.5)), ), ), ); @@ -712,7 +684,7 @@ class _MessageListViewState extends State { clipBehavior: Clip.none, children: [ FloatingActionButton( - backgroundColor: _streamTheme.colorTheme.white, + backgroundColor: _streamTheme.colorTheme.barsBg, onPressed: () { if (unreadCount > 0) { streamChannel!.channel.markRead(); @@ -731,7 +703,7 @@ class _MessageListViewState extends State { } }, child: StreamSvgIcon.down( - color: _streamTheme.colorTheme.black, + color: _streamTheme.colorTheme.textHighEmphasis, ), ), if (showUnreadCount) @@ -823,7 +795,6 @@ class _MessageListViewState extends State { showUsername: !isMyMessage, padding: const EdgeInsets.all(8), showSendingIndicator: false, - onThreadTap: _onThreadTap, borderRadiusGeometry: BorderRadius.only( topLeft: const Radius.circular(16), bottomLeft: @@ -841,7 +812,6 @@ class _MessageListViewState extends State { messageTheme: isMyMessage ? _streamTheme.ownMessageTheme : _streamTheme.otherMessageTheme, - onShowMessage: widget.onShowMessage, onReturnAction: (action) { switch (action) { case ReturnActionType.none: @@ -852,16 +822,12 @@ class _MessageListViewState extends State { break; } }, - customAttachmentBuilders: widget.customAttachmentBuilders, onMessageTap: (message) { if (widget.onMessageTap != null) { widget.onMessageTap!(message); } FocusScope.of(context).unfocus(); }, - textBuilder: widget.textBuilder, - usernameBuilder: widget.usernameBuilder, - onLinkTap: widget.onLinkTap, showPinButton: currentUserMember != null && widget.pinPermissions.contains(currentUserMember.role), ); @@ -899,7 +865,7 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user!.id; final isMyMessage = message.user!.id == userId; - final nextMessage = index - 3 >= 0 ? messages[index - 3] : null; + final nextMessage = index - 1 >= 0 ? messages[index - 1] : null; final isNextUserSame = nextMessage != null && message.user!.id == nextMessage.user!.id; @@ -1004,7 +970,6 @@ class _MessageListViewState extends State { showFlagButton: !isMyMessage, borderSide: borderSide, onThreadTap: _onThreadTap, - onReplyTap: widget.onReplyTap, attachmentBorderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(attachmentBorderRadius), bottomLeft: isMyMessage @@ -1055,7 +1020,6 @@ class _MessageListViewState extends State { : _streamTheme.otherMessageTheme, readList: readList, allRead: allRead, - onShowMessage: widget.onShowMessage, onReturnAction: (action) { switch (action) { case ReturnActionType.none: @@ -1066,17 +1030,12 @@ class _MessageListViewState extends State { break; } }, - customAttachmentBuilders: widget.customAttachmentBuilders, onMessageTap: (message) { if (widget.onMessageTap != null) { widget.onMessageTap!(message); } FocusScope.of(context).unfocus(); }, - onAttachmentTap: widget.onAttachmentTap, - textBuilder: widget.textBuilder, - usernameBuilder: widget.usernameBuilder, - onLinkTap: widget.onLinkTap, showPinButton: widget.pinPermissions.contains(currentUserMember.role), ); @@ -1108,7 +1067,7 @@ class _MessageListViewState extends State { widget.onMessageSwiped?.call(message); }, backgroundIcon: StreamSvgIcon.reply( - color: _streamTheme.colorTheme.accentBlue, + color: _streamTheme.colorTheme.accentPrimary, ), child: child, ), @@ -1124,7 +1083,7 @@ class _MessageListViewState extends State { child = TweenAnimationBuilder( tween: ColorTween( begin: highlightColor, - end: colorTheme.white.withOpacity(0), + end: colorTheme.barsBg.withOpacity(0), ), duration: const Duration(seconds: 3), onEnd: () => initialMessageHighlightComplete = true, @@ -1254,7 +1213,7 @@ class _LoadingIndicator extends StatelessWidget { stream: stream, initialData: false, errorBuilder: (context, error) => Container( - color: streamTheme.colorTheme.accentRed.withOpacity(.2), + color: streamTheme.colorTheme.accentError.withOpacity(.2), child: Center( child: Text(context.translations.loadingMessagesError), ), diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index f87b8740..7926dcab 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -143,7 +143,7 @@ class MessageReactionsModal extends StatelessWidget { Widget _buildReactionCard(BuildContext context, User? user) { final chatThemeData = StreamChatTheme.of(context); return Card( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), @@ -225,7 +225,7 @@ class MessageReactionsModal extends StatelessWidget { messageTheme.reactionsBorderColor ?? Colors.transparent, backgroundColor: messageTheme.reactionsBackgroundColor ?? Colors.transparent, - maskColor: chatThemeData.colorTheme.white, + maskColor: chatThemeData.colorTheme.barsBg, tailCirclesSpacing: 1, highlightOwnReactions: false, ), diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 9d6d36d6..b4321c7a 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -201,7 +201,7 @@ class _MessageSearchListViewState extends State { Widget _separatorBuilder(BuildContext context, int index) => Container( height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ); Widget _listItemBuilder( @@ -226,7 +226,7 @@ class _MessageSearchListViewState extends State { return Container( color: StreamChatTheme.of(context) .colorTheme - .accentRed + .accentError .withOpacity(.2), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), @@ -296,7 +296,7 @@ class _MessageSearchListViewState extends State { child: Text( context.translations.resultCountText(items.length), style: TextStyle( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 03c77a44..32e4794b 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -223,7 +223,7 @@ class MessageWidget extends StatefulWidget { final border = RoundedRectangleBorder( side: attachmentBorderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ), borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, ); @@ -750,7 +750,7 @@ class _MessageWidgetState extends State child: CustomPaint( painter: ReactionBubblePainter( _streamChatTheme - .colorTheme.white, + .colorTheme.barsBg, Colors.transparent, Colors.transparent, tailCirclesSpace: 1, @@ -836,14 +836,14 @@ class _MessageWidgetState extends State mainAxisSize: MainAxisSize.min, children: [ StreamSvgIcon.eye( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, size: 16, ), const SizedBox(width: 8), Text( context.translations.onlyVisibleToYouText, style: chatThemeData.textTheme.footnote - .copyWith(color: chatThemeData.colorTheme.grey), + .copyWith(color: chatThemeData.colorTheme.textLowEmphasis), ), ], ); @@ -1222,7 +1222,7 @@ class _MessageWidgetState extends State Text( widget.readList!.length.toString(), style: style.copyWith( - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), ), const SizedBox(width: 2), @@ -1299,7 +1299,7 @@ class _MessageWidgetState extends State currentUser: currentUser, ), style: TextStyle( - color: _streamChatTheme.colorTheme.grey, + color: _streamChatTheme.colorTheme.textLowEmphasis, fontSize: 13, fontWeight: FontWeight.w400, ), @@ -1317,7 +1317,7 @@ class _MessageWidgetState extends State } if (hasUrlAttachments) { - return _streamChatTheme.colorTheme.blueAlice; + return _streamChatTheme.colorTheme.linkBg; } if (isOnlyEmoji) { @@ -1373,7 +1373,7 @@ class _ThreadParticipants extends StatelessWidget { child: Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), padding: const EdgeInsets.all(1), child: UserAvatar( @@ -1402,7 +1402,7 @@ class _ThreadReplyPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() - ..color = color ?? StreamChatTheme.of(context!).colorTheme.greyGainsboro + ..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled ..style = PaintingStyle.stroke ..strokeWidth = 1 ..strokeCap = StrokeCap.round; diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 10ae1b5f..16bf9e97 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -46,11 +46,11 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: separatorColor ?? chatThemeData.colorTheme.greyGainsboro, + color: separatorColor ?? chatThemeData.colorTheme.disabled, height: 1, ), Material( - color: tileColor ?? chatThemeData.colorTheme.white, + color: tileColor ?? chatThemeData.colorTheme.barsBg, child: SizedBox( height: 63, child: InkWell( diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 43620640..22ca4d4c 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -154,7 +154,7 @@ class QuotedMessageWidget extends StatelessWidget { color: _getBackgroundColor(context), border: showBorder ? Border.all( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ) : null, borderRadius: BorderRadius.only( @@ -280,7 +280,7 @@ class QuotedMessageWidget extends StatelessWidget { Color? _getBackgroundColor(BuildContext context) { if (_containsScrapeUrl) { - return StreamChatTheme.of(context).colorTheme.blueAlice; + return StreamChatTheme.of(context).colorTheme.linkBg; } return messageTheme.messageBackgroundColor; } diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index a3b2052f..9e279344 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -141,8 +141,8 @@ class ReactionBubble extends StatelessWidget { Icons.help_outline_rounded, size: 16, color: (!highlightOwnReactions || reaction.user?.id == userId) - ? chatThemeData.colorTheme.accentBlue - : chatThemeData.colorTheme.black.withOpacity(.5), + ? chatThemeData.colorTheme.accentPrimary + : chatThemeData.colorTheme.textHighEmphasis.withOpacity(.5), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 5101cbb7..3e1e5e50 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -49,7 +49,7 @@ class _ReactionPickerState extends State final child = Material( borderRadius: BorderRadius.circular(24), - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, clipBehavior: Clip.hardEdge, child: Padding( padding: const EdgeInsets.symmetric( diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/sending_indicator.dart index 88560432..7f44e5b8 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/sending_indicator.dart @@ -25,7 +25,7 @@ class SendingIndicator extends StatelessWidget { if (isMessageRead) { return StreamSvgIcon.checkAll( size: size, - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ); } if (message.status == MessageSendingStatus.sent) { diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index dfffce88..f55f565d 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -101,8 +101,8 @@ class StreamChatState extends State { return Theme( data: materialTheme.copyWith( primaryIconTheme: streamTheme.primaryIconTheme, - accentColor: streamTheme.colorTheme.accentBlue, - scaffoldBackgroundColor: streamTheme.colorTheme.white, + accentColor: streamTheme.colorTheme.accentPrimary, + scaffoldBackgroundColor: streamTheme.colorTheme.barsBg, ), child: StreamChatCore( client: client, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 91ed2537..c62e92ec 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; @@ -54,10 +55,11 @@ class StreamChatThemeData { MessageTheme? otherMessageTheme, MessageTheme? ownMessageTheme, MessageInputTheme? messageInputTheme, - Widget Function(BuildContext, Channel)? defaultChannelImage, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, + GalleryHeaderThemeData? imageHeaderTheme, + GalleryFooterThemeData? imageFooterTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -76,10 +78,11 @@ class StreamChatThemeData { otherMessageTheme: otherMessageTheme, ownMessageTheme: ownMessageTheme, messageInputTheme: messageInputTheme, - defaultChannelImage: defaultChannelImage, defaultUserImage: defaultUserImage, primaryIconTheme: primaryIconTheme, reactionIcons: reactionIcons, + galleryHeaderTheme: imageHeaderTheme, + galleryFooterTheme: imageFooterTheme, ); return defaultData.merge(customizedData); @@ -103,10 +106,11 @@ class StreamChatThemeData { required this.otherMessageTheme, required this.ownMessageTheme, required this.messageInputTheme, - required this.defaultChannelImage, required this.defaultUserImage, required this.primaryIconTheme, required this.reactionIcons, + required this.galleryHeaderTheme, + required this.galleryFooterTheme, }); /// Create a theme from a Material [Theme] @@ -114,7 +118,7 @@ class StreamChatThemeData { final defaultTheme = StreamChatThemeData(brightness: theme.brightness); final customizedTheme = StreamChatThemeData.fromColorAndTextTheme( defaultTheme.colorTheme.copyWith( - accentBlue: theme.accentColor, + accentPrimary: theme.accentColor, ), defaultTheme.textTheme, ); @@ -136,6 +140,14 @@ class StreamChatThemeData { /// Theme of the chat widgets dedicated to a channel final ChannelTheme channelTheme; + /// The default style for [GalleryHeader]s below the overall + /// [StreamChatTheme]. + final GalleryHeaderThemeData galleryHeaderTheme; + + /// The default style for [GalleryFooter]s below the overall + /// [StreamChatTheme]. + final GalleryFooterThemeData galleryFooterTheme; + /// Theme of the current user messages final MessageTheme ownMessageTheme; @@ -145,9 +157,6 @@ class StreamChatThemeData { /// Theme dedicated to the [MessageInput] widget final MessageInputTheme messageInputTheme; - /// The widget that will be built when the channel image is unavailable - final Widget Function(BuildContext, Channel) defaultChannelImage; - /// The widget that will be built when the user image is unavailable final Widget Function(BuildContext, User) defaultUserImage; @@ -167,11 +176,12 @@ class StreamChatThemeData { MessageTheme? ownMessageTheme, MessageTheme? otherMessageTheme, MessageInputTheme? messageInputTheme, - Widget Function(BuildContext, Channel)? defaultChannelImage, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, ChannelListHeaderTheme? channelListHeaderTheme, List? reactionIcons, + GalleryHeaderThemeData? galleryHeaderTheme, + GalleryFooterThemeData? galleryFooterTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -179,7 +189,6 @@ class StreamChatThemeData { textTheme: this.textTheme.merge(textTheme), colorTheme: this.colorTheme.merge(colorTheme), primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), - defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage, defaultUserImage: defaultUserImage ?? this.defaultUserImage, channelPreviewTheme: this.channelPreviewTheme.merge(channelPreviewTheme), @@ -188,6 +197,8 @@ class StreamChatThemeData { otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), messageInputTheme: this.messageInputTheme.merge(messageInputTheme), reactionIcons: reactionIcons ?? this.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, + galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, ); /// Merge themes @@ -199,7 +210,6 @@ class StreamChatThemeData { textTheme: textTheme.merge(other.textTheme), colorTheme: colorTheme.merge(other.colorTheme), primaryIconTheme: other.primaryIconTheme, - defaultChannelImage: other.defaultChannelImage, defaultUserImage: other.defaultUserImage, channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), channelTheme: channelTheme.merge(other.channelTheme), @@ -207,6 +217,8 @@ class StreamChatThemeData { otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), messageInputTheme: messageInputTheme.merge(other.messageInputTheme), reactionIcons: other.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), + galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), ); } @@ -216,22 +228,11 @@ class StreamChatThemeData { ColorTheme colorTheme, TextTheme textTheme, ) { - final accentColor = colorTheme.accentBlue; - final iconTheme = IconThemeData(color: colorTheme.black.withOpacity(.5)); - return StreamChatThemeData.raw( - textTheme: textTheme, - colorTheme: colorTheme, - primaryIconTheme: iconTheme, - defaultChannelImage: (context, channel) => const SizedBox(), - defaultUserImage: (context, user) => Center( - child: CachedNetworkImage( - filterQuality: FilterQuality.high, - imageUrl: getRandomPicUrl(user), - fit: BoxFit.cover, - ), - ), - channelPreviewTheme: ChannelPreviewTheme( - unreadCounterColor: colorTheme.accentRed, + final accentColor = colorTheme.accentPrimary; + final iconTheme = + IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); + final channelTheme = ChannelTheme( + channelHeaderTheme: ChannelHeaderTheme( avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -239,15 +240,43 @@ class StreamChatThemeData { width: 40, ), ), - title: textTheme.bodyBold, + color: colorTheme.barsBg, + title: textTheme.headlineBold, subtitle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAt: textTheme.footnote.copyWith( - color: colorTheme.black.withOpacity(.5), - ), - indicatorIconSize: 16, ), + ); + final channelPreviewTheme = ChannelPreviewTheme( + unreadCounterColor: colorTheme.accentError, + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: textTheme.bodyBold, + subtitle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAt: textTheme.footnote.copyWith( + color: colorTheme.textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, + ); + return StreamChatThemeData.raw( + textTheme: textTheme, + colorTheme: colorTheme, + primaryIconTheme: iconTheme, + defaultUserImage: (context, user) => Center( + child: CachedNetworkImage( + filterQuality: FilterQuality.high, + imageUrl: getRandomPicUrl(user), + fit: BoxFit.cover, + ), + ), + channelPreviewTheme: channelPreviewTheme, channelListHeaderTheme: ChannelListHeaderTheme( avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), @@ -256,35 +285,22 @@ class StreamChatThemeData { width: 40, ), ), - color: colorTheme.white, + color: colorTheme.barsBg, title: textTheme.headlineBold, ), - channelTheme: ChannelTheme( - channelHeaderTheme: ChannelHeaderTheme( - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - color: colorTheme.white, - title: textTheme.headlineBold, - subtitle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - ), - ), + channelTheme: channelTheme, ownMessageTheme: MessageTheme( - messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey), + messageAuthor: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), messageText: textTheme.body, - createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), + createdAt: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), replies: textTheme.footnoteBold.copyWith(color: accentColor), - messageBackgroundColor: colorTheme.greyGainsboro, - reactionsBackgroundColor: colorTheme.white, - reactionsBorderColor: colorTheme.greyWhisper, - reactionsMaskColor: colorTheme.whiteSnow, - messageBorderColor: colorTheme.greyGainsboro, + messageBackgroundColor: colorTheme.disabled, + reactionsBackgroundColor: colorTheme.barsBg, + reactionsBorderColor: colorTheme.borders, + reactionsMaskColor: colorTheme.appBg, + messageBorderColor: colorTheme.disabled, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -297,18 +313,20 @@ class StreamChatThemeData { ), ), otherMessageTheme: MessageTheme( - reactionsBackgroundColor: colorTheme.greyGainsboro, - reactionsBorderColor: colorTheme.white, - reactionsMaskColor: colorTheme.whiteSnow, + reactionsBackgroundColor: colorTheme.disabled, + reactionsBorderColor: colorTheme.barsBg, + reactionsMaskColor: colorTheme.appBg, messageText: textTheme.body, - createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), - messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey), + createdAt: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + messageAuthor: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), replies: textTheme.footnoteBold.copyWith(color: accentColor), messageLinks: TextStyle( color: accentColor, ), - messageBackgroundColor: colorTheme.white, - messageBorderColor: colorTheme.greyWhisper, + messageBackgroundColor: colorTheme.barsBg, + messageBorderColor: colorTheme.borders, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -320,23 +338,23 @@ class StreamChatThemeData { messageInputTheme: MessageInputTheme( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: colorTheme.accentBlue, - actionButtonIdleColor: colorTheme.grey, - expandButtonColor: colorTheme.accentBlue, - sendButtonColor: colorTheme.accentBlue, - sendButtonIdleColor: colorTheme.greyGainsboro, - inputBackground: colorTheme.white, + actionButtonColor: colorTheme.accentPrimary, + actionButtonIdleColor: colorTheme.textLowEmphasis, + expandButtonColor: colorTheme.accentPrimary, + sendButtonColor: colorTheme.accentPrimary, + sendButtonIdleColor: colorTheme.disabled, + inputBackground: colorTheme.barsBg, inputTextStyle: textTheme.body, idleBorderGradient: LinearGradient( colors: [ - colorTheme.greyGainsboro, - colorTheme.greyGainsboro, + colorTheme.disabled, + colorTheme.disabled, ], ), activeBorderGradient: LinearGradient( colors: [ - colorTheme.greyGainsboro, - colorTheme.greyGainsboro, + colorTheme.disabled, + colorTheme.disabled, ], ), ), @@ -347,7 +365,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.loveReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -359,7 +377,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.thumbsUpReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -371,7 +389,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.thumbsDownReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -383,7 +401,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.lolReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -395,13 +413,31 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.wutReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); }, ), ], + galleryHeaderTheme: GalleryHeaderThemeData( + closeButtonColor: colorTheme.textHighEmphasis, + backgroundColor: channelTheme.channelHeaderTheme.color, + iconMenuPointColor: colorTheme.textHighEmphasis, + titleTextStyle: textTheme.headlineBold, + subtitleTextStyle: channelPreviewTheme.subtitle, + bottomSheetBarrierColor: colorTheme.overlay, + ), + galleryFooterTheme: GalleryFooterThemeData( + backgroundColor: colorTheme.barsBg, + shareIconColor: colorTheme.textHighEmphasis, + titleTextStyle: textTheme.headlineBold, + gridIconButtonColor: colorTheme.textHighEmphasis, + bottomSheetBarrierColor: colorTheme.overlay, + bottomSheetBackgroundColor: colorTheme.barsBg, + bottomSheetPhotosTextStyle: textTheme.headlineBold, + bottomSheetCloseIconColor: colorTheme.textHighEmphasis, + ), ); } } @@ -572,17 +608,17 @@ class TextTheme { class ColorTheme { /// Initialise with light theme ColorTheme.light({ - this.black = const Color(0xff000000), - this.grey = const Color(0xff7a7a7a), - this.greyGainsboro = const Color(0xffdbdbdb), - this.greyWhisper = const Color(0xffecebeb), - this.whiteSmoke = const Color(0xfff2f2f2), - this.whiteSnow = const Color(0xfffcfcfc), - this.white = const Color(0xffffffff), - this.blueAlice = const Color(0xffe9f2ff), - this.accentBlue = const Color(0xff005FFF), - this.accentRed = const Color(0xffFF3842), - this.accentGreen = const Color(0xff20E070), + this.textHighEmphasis = const Color(0xff000000), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xffdbdbdb), + this.borders = const Color(0xffecebeb), + this.inputBg = const Color(0xfff2f2f2), + this.appBg = const Color(0xfffcfcfc), + this.barsBg = const Color(0xffffffff), + this.linkBg = const Color(0xffe9f2ff), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3842), + this.accentInfo = const Color(0xff20E070), this.highlight = const Color(0xfffbf4dd), this.overlay = const Color.fromRGBO(0, 0, 0, 0.2), this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6), @@ -604,17 +640,17 @@ class ColorTheme { /// Initialise with dark theme ColorTheme.dark({ - this.black = const Color(0xffffffff), - this.grey = const Color(0xff7a7a7a), - this.greyGainsboro = const Color(0xff2d2f2f), - this.greyWhisper = const Color(0xff1c1e22), - this.whiteSmoke = const Color(0xff13151b), - this.whiteSnow = const Color(0xff070A0D), - this.white = const Color(0xff101418), - this.blueAlice = const Color(0xff00193D), - this.accentBlue = const Color(0xff005FFF), - this.accentRed = const Color(0xffFF3742), - this.accentGreen = const Color(0xff20E070), + this.textHighEmphasis = const Color(0xffffffff), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xff2d2f2f), + this.borders = const Color(0xff1c1e22), + this.inputBg = const Color(0xff13151b), + this.appBg = const Color(0xff070A0D), + this.barsBg = const Color(0xff101418), + this.linkBg = const Color(0xff00193D), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3742), + this.accentInfo = const Color(0xff20E070), this.borderTop = const Effect( sigmaX: 0, sigmaY: -1, @@ -657,37 +693,37 @@ class ColorTheme { }) : brightness = Brightness.dark; /// - final Color black; + final Color textHighEmphasis; /// - final Color grey; + final Color textLowEmphasis; /// - final Color greyGainsboro; + final Color disabled; /// - final Color greyWhisper; + final Color borders; /// - final Color whiteSmoke; + final Color inputBg; /// - final Color whiteSnow; + final Color appBg; /// - final Color white; + final Color barsBg; /// - final Color blueAlice; + final Color linkBg; /// - final Color accentBlue; + final Color accentPrimary; /// - final Color accentRed; + final Color accentError; /// - final Color accentGreen; + final Color accentInfo; /// final Effect borderTop; @@ -719,17 +755,17 @@ class ColorTheme { /// Copy with theme ColorTheme copyWith({ Brightness brightness = Brightness.light, - Color? black, - Color? grey, - Color? greyGainsboro, - Color? greyWhisper, - Color? whiteSmoke, - Color? whiteSnow, - Color? white, - Color? blueAlice, - Color? accentBlue, - Color? accentRed, - Color? accentGreen, + Color? textHighEmphasis, + Color? textLowEmphasis, + Color? disabled, + Color? borders, + Color? inputBg, + Color? appBg, + Color? barsBg, + Color? linkBg, + Color? accentPrimary, + Color? accentError, + Color? accentInfo, Effect? borderTop, Effect? borderBottom, Effect? shadowIconButton, @@ -741,17 +777,17 @@ class ColorTheme { }) => brightness == Brightness.light ? ColorTheme.light( - black: black ?? this.black, - grey: grey ?? this.grey, - greyGainsboro: greyGainsboro ?? this.greyGainsboro, - greyWhisper: greyWhisper ?? this.greyWhisper, - whiteSmoke: whiteSmoke ?? this.whiteSmoke, - whiteSnow: whiteSnow ?? this.whiteSnow, - white: white ?? this.white, - blueAlice: blueAlice ?? this.blueAlice, - accentBlue: accentBlue ?? this.accentBlue, - accentRed: accentRed ?? this.accentRed, - accentGreen: accentGreen ?? this.accentGreen, + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, borderTop: borderTop ?? this.borderTop, borderBottom: borderBottom ?? this.borderBottom, shadowIconButton: shadowIconButton ?? this.shadowIconButton, @@ -762,17 +798,17 @@ class ColorTheme { bgGradient: bgGradient ?? this.bgGradient, ) : ColorTheme.dark( - black: black ?? this.black, - grey: grey ?? this.grey, - greyGainsboro: greyGainsboro ?? this.greyGainsboro, - greyWhisper: greyWhisper ?? this.greyWhisper, - whiteSmoke: whiteSmoke ?? this.whiteSmoke, - whiteSnow: whiteSnow ?? this.whiteSnow, - white: white ?? this.white, - blueAlice: blueAlice ?? this.blueAlice, - accentBlue: accentBlue ?? this.accentBlue, - accentRed: accentRed ?? this.accentRed, - accentGreen: accentGreen ?? this.accentGreen, + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, borderTop: borderTop ?? this.borderTop, borderBottom: borderBottom ?? this.borderBottom, shadowIconButton: shadowIconButton ?? this.shadowIconButton, @@ -787,17 +823,17 @@ class ColorTheme { ColorTheme merge(ColorTheme? other) { if (other == null) return this; return copyWith( - black: other.black, - grey: other.grey, - greyGainsboro: other.greyGainsboro, - greyWhisper: other.greyWhisper, - whiteSmoke: other.whiteSmoke, - whiteSnow: other.whiteSnow, - white: other.white, - blueAlice: other.blueAlice, - accentBlue: other.accentBlue, - accentRed: other.accentRed, - accentGreen: other.accentGreen, + textHighEmphasis: other.textHighEmphasis, + textLowEmphasis: other.textLowEmphasis, + disabled: other.disabled, + borders: other.borders, + inputBg: other.inputBg, + appBg: other.appBg, + barsBg: other.barsBg, + linkBg: other.linkBg, + accentPrimary: other.accentPrimary, + accentError: other.accentError, + accentInfo: other.accentInfo, highlight: other.highlight, overlay: other.overlay, overlayDark: other.overlayDark, @@ -1282,3 +1318,393 @@ class Effect { blur: blur ?? this.blur, ); } + +/// Overrides the default style of [GalleryHeader] descendants. +/// +/// See also: +/// +/// * [GalleryHeaderThemeData], which is used to configure this theme. +class GalleryHeaderTheme extends InheritedTheme { + /// Creates an [GalleryHeaderTheme]. + /// + /// The [data] parameter must not be null. + const GalleryHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// [StreamChatThemeData.galleryHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); + /// ``` + static GalleryHeaderThemeData of(BuildContext context) { + final galleryHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return galleryHeaderTheme?.data ?? + StreamChatTheme.of(context).galleryHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryHeader]s when used +/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryHeaderTheme]. +/// +/// See also: +/// +/// * [GalleryHeaderTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override +/// the default style for [GalleryHeader]s below the overall [StreamChatTheme]. +class GalleryHeaderThemeData with Diagnosticable { + /// Creates an [GalleryHeaderThemeData]. + const GalleryHeaderThemeData({ + this.closeButtonColor, + this.backgroundColor, + this.iconMenuPointColor, + this.titleTextStyle, + this.subtitleTextStyle, + this.bottomSheetBarrierColor, + }); + + /// The color of the "close" button. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? closeButtonColor; + + /// The background color of the [GalleryHeader] widget. + /// + /// Defaults to [ChannelHeaderTheme.color]. + final Color? backgroundColor; + + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? iconMenuPointColor; + + /// The [TextStyle] to use for the [GalleryHeader] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The [TextStyle] to use for the [GalleryHeader] subtitle text. + /// + /// Defaults to [ChannelPreviewTheme.subtitle]. + final TextStyle? subtitleTextStyle; + + /// + final Color? bottomSheetBarrierColor; + + /// Copies this [GalleryHeaderThemeData] to another. + GalleryHeaderThemeData copyWith({ + Color? closeButtonColor, + Color? backgroundColor, + Color? iconMenuPointColor, + TextStyle? titleTextStyle, + TextStyle? subtitleTextStyle, + Color? bottomSheetBarrierColor, + }) => + GalleryHeaderThemeData( + closeButtonColor: closeButtonColor ?? this.closeButtonColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + ); + + /// Linearly interpolate between two [GalleryHeader] themes. + /// + /// All the properties must be non-null. + GalleryHeaderThemeData lerp( + GalleryHeaderThemeData a, + GalleryHeaderThemeData b, + double t, + ) => + GalleryHeaderThemeData( + closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + iconMenuPointColor: + Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + subtitleTextStyle: + TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + ); + + /// Merges one [GalleryHeaderThemeData] with the another + GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { + if (other == null) return this; + return copyWith( + closeButtonColor: other.closeButtonColor, + backgroundColor: other.backgroundColor, + iconMenuPointColor: other.iconMenuPointColor, + titleTextStyle: other.titleTextStyle, + subtitleTextStyle: other.subtitleTextStyle, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryHeaderThemeData && + runtimeType == other.runtimeType && + closeButtonColor == other.closeButtonColor && + backgroundColor == other.backgroundColor && + iconMenuPointColor == other.iconMenuPointColor && + titleTextStyle == other.titleTextStyle && + subtitleTextStyle == other.subtitleTextStyle && + bottomSheetBarrierColor == other.bottomSheetBarrierColor; + + @override + int get hashCode => + closeButtonColor.hashCode ^ + backgroundColor.hashCode ^ + iconMenuPointColor.hashCode ^ + titleTextStyle.hashCode ^ + subtitleTextStyle.hashCode ^ + bottomSheetBarrierColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('closeButtonColor', closeButtonColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('iconMenuPointColor', iconMenuPointColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)); + } +} + +/// Overrides the default style of [GalleryFooter] descendants. +/// +/// See also: +/// +/// * [GalleryFooterThemeData], which is used to configure this theme. +class GalleryFooterTheme extends InheritedTheme { + /// Creates an [GalleryFooterTheme]. + /// + /// The [data] parameter must not be null. + const GalleryFooterTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryFooterThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryFooterTheme] widget, then + /// [StreamChatThemeData.galleryFooterTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageFooterTheme theme = ImageFooterTheme.of(context); + /// ``` + static GalleryFooterThemeData of(BuildContext context) { + final imageFooterTheme = + context.dependOnInheritedWidgetOfExactType(); + return imageFooterTheme?.data ?? + StreamChatTheme.of(context).galleryFooterTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryFooterTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryFooterTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryFooter]s when used +/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryFooterTheme]. +/// +/// See also: +/// +/// * [GalleryFooterTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override +/// the default style for [GalleryFooter]s below the overall [StreamChatTheme]. +class GalleryFooterThemeData with Diagnosticable { + /// Creates an [GalleryFooterThemeData]. + const GalleryFooterThemeData({ + this.backgroundColor, + this.shareIconColor, + this.titleTextStyle, + this.gridIconButtonColor, + this.bottomSheetBarrierColor, + this.bottomSheetBackgroundColor, + this.bottomSheetPhotosTextStyle, + this.bottomSheetCloseIconColor, + }); + + /// The background color for the [GalleryFooter] widget. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? backgroundColor; + + /// The color for the "share" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? shareIconColor; + + /// The [TextStyle] to use for the [GalleryFooter] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The color to use for the "grid" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? gridIconButtonColor; + + /// The color to use behind the bottom sheet. + /// + /// Defaults to [ColorTheme.overlay]. + final Color? bottomSheetBarrierColor; + + /// The background color to use for the bottom sheet. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? bottomSheetBackgroundColor; + + /// The [TextStyle] to use for the "photos" text in the bottom sheet. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? bottomSheetPhotosTextStyle; + + /// The color to use for the "close" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? bottomSheetCloseIconColor; + + /// Copies this [GalleryFooterThemeData] to another. + GalleryFooterThemeData copyWith({ + Color? backgroundColor, + Color? shareIconColor, + TextStyle? titleTextStyle, + Color? gridIconButtonColor, + Color? bottomSheetBarrierColor, + Color? bottomSheetBackgroundColor, + TextStyle? bottomSheetPhotosTextStyle, + Color? bottomSheetCloseIconColor, + }) => + GalleryFooterThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + shareIconColor: shareIconColor ?? this.shareIconColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBackgroundColor: + bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, + bottomSheetPhotosTextStyle: + bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, + bottomSheetCloseIconColor: + bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, + ); + + /// Linearly interpolate between two [GalleryFooter] themes. + /// + /// All the properties must be non-null. + GalleryFooterThemeData lerp( + GalleryFooterThemeData a, + GalleryFooterThemeData b, + double t, + ) => + GalleryFooterThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + gridIconButtonColor: + Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + bottomSheetBackgroundColor: Color.lerp( + a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t), + bottomSheetPhotosTextStyle: TextStyle.lerp( + a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t), + bottomSheetCloseIconColor: Color.lerp( + a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), + ); + + /// Merges one [GalleryFooterThemeData] with the another + GalleryFooterThemeData merge(GalleryFooterThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + bottomSheetBackgroundColor: other.bottomSheetBackgroundColor, + bottomSheetCloseIconColor: other.bottomSheetCloseIconColor, + bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle, + gridIconButtonColor: other.gridIconButtonColor, + titleTextStyle: other.titleTextStyle, + shareIconColor: other.shareIconColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryFooterThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor && + shareIconColor == other.shareIconColor && + titleTextStyle == other.titleTextStyle && + gridIconButtonColor == other.gridIconButtonColor && + bottomSheetBarrierColor == other.bottomSheetBarrierColor && + bottomSheetBackgroundColor == other.bottomSheetBackgroundColor && + bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle && + bottomSheetCloseIconColor == other.bottomSheetCloseIconColor; + + @override + int get hashCode => + backgroundColor.hashCode ^ + shareIconColor.hashCode ^ + titleTextStyle.hashCode ^ + gridIconButtonColor.hashCode ^ + bottomSheetBarrierColor.hashCode ^ + bottomSheetBackgroundColor.hashCode ^ + bottomSheetPhotosTextStyle.hashCode ^ + bottomSheetCloseIconColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('shareIconColor', shareIconColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) + ..add(ColorProperty( + 'bottomSheetBackgroundColor', bottomSheetBackgroundColor)) + ..add(DiagnosticsProperty( + 'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle)) + ..add(ColorProperty( + 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/swipeable.dart b/packages/stream_chat_flutter/lib/src/swipeable.dart index 5895e8b5..041111e4 100644 --- a/packages/stream_chat_flutter/lib/src/swipeable.dart +++ b/packages/stream_chat_flutter/lib/src/swipeable.dart @@ -151,9 +151,8 @@ class _SwipeableState extends State with TickerProviderStateMixin { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, + color: + StreamChatTheme.of(context).colorTheme.disabled, ), ), child: widget.backgroundIcon, diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index b847eb3c..11c80300 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -32,7 +32,7 @@ class SystemMessage extends StatelessWidget { textAlign: TextAlign.center, softWrap: true, style: theme.textTheme.captionBold.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart index 9905141a..b3fe510d 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart @@ -59,7 +59,7 @@ class UploadProgressIndicator extends StatelessWidget { '${_percentage.toInt()}%', style: textStyle ?? theme.textTheme.footnote.copyWith( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/url_attachment.dart b/packages/stream_chat_flutter/lib/src/url_attachment.dart index 4944c73c..5d5ca489 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/url_attachment.dart @@ -60,7 +60,7 @@ class UrlAttachment extends StatelessWidget { borderRadius: const BorderRadius.only( topRight: Radius.circular(16), ), - color: chatThemeData.colorTheme.blueAlice, + color: chatThemeData.colorTheme.linkBg, ), child: Padding( padding: const EdgeInsets.only( @@ -71,7 +71,7 @@ class UrlAttachment extends StatelessWidget { child: Text( hostDisplayName, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index ecf1783f..31e07f0a 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -70,7 +70,7 @@ class UserAvatar extends StatelessWidget { constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentBlue, + color: streamChatTheme.colorTheme.accentPrimary, ), child: hasImage ? CachedNetworkImage( @@ -95,7 +95,7 @@ class UserAvatar extends StatelessWidget { child: Container( constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - color: selectionColor ?? streamChatTheme.colorTheme.accentBlue, + color: selectionColor ?? streamChatTheme.colorTheme.accentPrimary, child: Padding( padding: EdgeInsets.all(selectionThickness), child: avatar, @@ -115,7 +115,7 @@ class UserAvatar extends StatelessWidget { alignment: onlineIndicatorAlignment, child: Material( type: MaterialType.circle, - color: streamChatTheme.colorTheme.white, + color: streamChatTheme.colorTheme.barsBg, child: Container( margin: const EdgeInsets.all(2), constraints: onlineIndicatorConstraints ?? @@ -125,7 +125,7 @@ class UserAvatar extends StatelessWidget { ), child: Material( shape: const CircleBorder(), - color: streamChatTheme.colorTheme.accentGreen, + color: streamChatTheme.colorTheme.accentInfo, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index 4b42eb6a..99b35df1 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -76,7 +76,7 @@ class UserItem extends StatelessWidget { ), trailing: selected ? StreamSvgIcon.checkSend( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ) : null, title: Text( @@ -94,8 +94,8 @@ class UserItem extends StatelessWidget { ? context.translations.userOnlineText : context.translations.userLastOnlineText + Jiffy(user.lastActive).fromNow(), - style: chatTheme.textTheme.footnote - .copyWith(color: chatTheme.colorTheme.black.withOpacity(.5)), + style: chatTheme.textTheme.footnote.copyWith( + color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)), ); } } diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 3ad1a337..cc6051c6 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -287,7 +287,7 @@ class _UserListViewState extends State final chatThemeData = StreamChatTheme.of(context); return Container( key: ValueKey('HEADER-$header'), - color: chatThemeData.colorTheme.black.withOpacity(0.05), + color: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.05), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Text( @@ -295,7 +295,7 @@ class _UserListViewState extends State style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14.5, - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), @@ -387,7 +387,7 @@ class _UserListViewState extends State return Container( color: StreamChatTheme.of(context) .colorTheme - .accentRed + .accentError .withOpacity(.2), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), @@ -410,6 +410,6 @@ class _UserListViewState extends State Widget _separatorBuilder(context, i) => Container( height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ); } diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index e6fda991..314a788a 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -29,7 +29,7 @@ Future showConfirmationDialog( final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( useRootNavigator: false, - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: chatThemeData.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -73,7 +73,7 @@ Future showConfirmationDialog( child: Text( cancelText, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.black + color: chatThemeData.colorTheme.textHighEmphasis .withOpacity(0.5)), ), ), @@ -89,7 +89,7 @@ Future showConfirmationDialog( child: Text( okText, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentRed), + color: chatThemeData.colorTheme.accentError), ), ), ), @@ -114,7 +114,8 @@ Future showInfoDialog( final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( useRootNavigator: false, - backgroundColor: theme?.colorTheme.white ?? chatThemeData.colorTheme.white, + backgroundColor: + theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -145,8 +146,8 @@ Future showInfoDialog( height: 36, ), Container( - color: theme?.colorTheme.black.withOpacity(.08) ?? - chatThemeData.colorTheme.black.withOpacity(.08), + color: theme?.colorTheme.textHighEmphasis.withOpacity(.08) ?? + chatThemeData.colorTheme.textHighEmphasis.withOpacity(.08), height: 1, ), Center( @@ -157,8 +158,8 @@ Future showInfoDialog( child: Text( okText, style: TextStyle( - color: theme?.colorTheme.black.withOpacity(0.5) ?? - chatThemeData.colorTheme.accentBlue, + color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ?? + chatThemeData.colorTheme.accentPrimary, fontWeight: FontWeight.w400, ), ), diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart index 290dd54b..e914f466 100644 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ b/packages/stream_chat_flutter/lib/src/video_service.dart @@ -16,21 +16,25 @@ class IVideoService { /// compress video from [path] /// compress video from [path] return [Future] /// - /// you can choose its quality by [quality], - /// determine whether to delete his source file by [deleteOrigin] - /// optional parameters [startTime] [duration] [includeAudio] [frameRate] + /// you can choose its [quality] and [frameRate] /// /// ## example /// ```dart /// final info = await _flutterVideoCompress.compressVideo( /// file.path, - /// deleteOrigin: true, /// ); /// debugPrint(info.toJson()); /// ``` - Future compressVideo(String path) async => _lock.synchronized( + Future compressVideo( + String path, { + int frameRate = 30, + VideoQuality quality = VideoQuality.DefaultQuality, + }) async => + _lock.synchronized( () => VideoCompress.compressVideo( path, + frameRate: frameRate, + quality: quality, ), ); diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart index 23cc0e35..2286bc3d 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart @@ -94,8 +94,8 @@ class _VideoThumbnailImageState extends State { constraints: const BoxConstraints.expand(), child: widget.placeholderBuilder?.call(context) ?? Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.greyGainsboro, - highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, child: Image.asset( 'images/placeholder.png', fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index af8e66d7..4b4fd9eb 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -2,8 +2,8 @@ export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; export 'src/back_button.dart'; +export 'src/channel_avatar.dart'; export 'src/channel_header.dart'; -export 'src/channel_image.dart'; export 'src/channel_list_header.dart'; export 'src/channel_list_view.dart'; export 'src/channel_name.dart'; @@ -12,8 +12,8 @@ export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; export 'src/full_screen_media.dart'; -export 'src/image_footer.dart'; -export 'src/image_header.dart'; +export 'src/gallery_footer.dart'; +export 'src/gallery_header.dart'; export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/mention_tile.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index e15f51c7..0ac54486 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.0.0-nullsafety.8 +version: 2.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -26,7 +26,7 @@ dependencies: flutter_svg: ^0.22.0 http_parser: ^4.0.0 image_gallery_saver: ^1.6.9 - image_picker: ^0.8.0 + image_picker: ^0.8.2 jiffy: ^4.1.0 lottie: ^1.0.1 meta: ^1.3.0 @@ -37,7 +37,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0-nullsafety.8 + stream_chat_flutter_core: ^2.0.0 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 87f3d028..211a7132 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -14,10 +14,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -63,7 +65,7 @@ void main() { )); expect(find.text('test'), findsOneWidget); - expect(find.byType(ChannelImage), findsOneWidget); + expect(find.byType(ChannelAvatar), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); expect(find.byType(ChannelInfo), findsOneWidget); }, @@ -76,10 +78,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -141,10 +145,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -207,10 +213,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -263,7 +271,7 @@ void main() { expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); - expect(find.byType(ChannelImage), findsNothing); + expect(find.byType(ChannelAvatar), findsNothing); expect(find.byType(ChannelInfo), findsNothing); expect(find.text('leading'), findsOneWidget); expect(find.text('title'), findsOneWidget); @@ -281,10 +289,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -346,10 +356,12 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -403,7 +415,7 @@ void main() { )); await tester.tap(find.byType(StreamBackButton)); - await tester.tap(find.byType(ChannelImage)); + await tester.tap(find.byType(ChannelAvatar)); await tester.tap(find.byType(ChannelName)); expect(backPressed, true); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index ba913a77..7c620774 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/group_image.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; @@ -35,7 +35,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelImage(), + body: ChannelAvatar(), ), ), ), @@ -74,7 +74,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: { + extraData: const { 'image': 'testimage', }, ), @@ -85,7 +85,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: { + extraData: const { 'image': 'testimage', }, ), @@ -98,7 +98,7 @@ void main() { when(() => clientState.usersStream).thenAnswer((i) => Stream.value({ 'user-id2': User( id: 'user-id2', - extraData: { + extraData: const { 'image': 'testimage', }, ), @@ -113,7 +113,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelImage(), + body: ChannelAvatar(), ), ), ), @@ -132,9 +132,10 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final currentUser = OwnUser(id: 'user-id'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(currentUser); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ @@ -143,41 +144,12 @@ void main() { when(() => channel.extraData).thenReturn({ 'name': 'test', }); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User( - id: 'user-id', - extraData: { - 'image': 'testimage1', - }, - ), - ), - Member( - userId: 'user-id2', - user: User( - id: 'user-id2', - extraData: { - 'image': 'testimage2', - }, - ), - ), - Member( - userId: 'user-id3', - user: User( - id: 'user-id3', - extraData: { - 'image': 'testimage3', - }, - ), - ), - ])); - when(() => channelState.members).thenReturn([ + final members = [ Member( userId: 'user-id', user: User( id: 'user-id', - extraData: { + extraData: const { 'image': 'testimage1', }, ), @@ -186,7 +158,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: { + extraData: const { 'image': 'testimage2', }, ), @@ -195,12 +167,15 @@ void main() { userId: 'user-id3', user: User( id: 'user-id3', - extraData: { + extraData: const { 'image': 'testimage3', }, ), ), - ]); + ]; + when(() => channelState.members).thenReturn(members); + when(() => channelState.membersStream) + .thenAnswer((_) => Stream.value(members)); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -208,17 +183,18 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelImage(), + body: ChannelAvatar(), ), ), ), )); - final image = tester.widget(find.byType(GroupImage)); - expect(image.images, [ - 'testimage2', - 'testimage3', - ]); + final image = tester.widget(find.byType(GroupAvatar)); + final otherMembers = members.where((it) => it.userId != currentUser.id); + expect( + image.members.map((it) => it.user?.id), + otherMembers.map((it) => it.user?.id), + ); }, ); @@ -249,7 +225,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelImage( + body: ChannelAvatar( selected: true, ), ), diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index baa82614..6add0c60 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -13,11 +13,13 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); + final user = OwnUser(id: 'user-id'); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => channel.cid).thenReturn('cid'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.user).thenReturn(user); + when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -78,7 +80,7 @@ void main() { expect(find.text('test name'), findsOneWidget); expect(find.text('1'), findsOneWidget); expect(find.text('hello'), findsOneWidget); - expect(find.byType(ChannelImage), findsOneWidget); + expect(find.byType(ChannelAvatar), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index 206d92e3..0e30d2b3 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -70,6 +70,7 @@ void main() { StreamChat( streamChatThemeData: theme, client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: StreamChannel( showLoading: false, channel: channel, @@ -79,7 +80,6 @@ void main() { ), ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -123,6 +123,7 @@ void main() { StreamChat( streamChatThemeData: theme, client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: StreamChannel( showLoading: false, channel: channel, @@ -132,7 +133,6 @@ void main() { ), ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -176,6 +176,7 @@ void main() { StreamChat( streamChatThemeData: theme, client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: StreamChannel( showLoading: false, channel: channel, @@ -189,7 +190,6 @@ void main() { ), ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 3e6f1743..03f6ea65 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -54,12 +54,12 @@ void main() { ) ])); when(() => channelState.typingEvents).thenAnswer((i) => { - User(id: 'other-user', extraData: {'name': 'demo'}): + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }); when(() => channelState.typingEventsStream) .thenAnswer((i) => Stream.value({ - User(id: 'other-user', extraData: {'name': 'demo'}): + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), })); diff --git a/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart new file mode 100644 index 00000000..fb8d7035 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockStreamChatClient extends Mock implements StreamChatClient {} + +void main() { + test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { + expect(const GalleryFooterThemeData(), + const GalleryFooterThemeData().copyWith()); + expect(const GalleryFooterThemeData().hashCode, + const GalleryFooterThemeData().copyWith().hashCode); + }); + + test( + '''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, 1), + _galleryFooterThemeDataControlDark); + }); + + test( + '''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, 0.5), + _galleryFooterThemeDataControlMidLerp); + }); + + test( + '''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', + () { + expect( + const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, + _galleryFooterThemeDataControl, 1), + _galleryFooterThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryFooterThemeDataControl + .merge(_galleryFooterThemeDataControlDark), + _galleryFooterThemeDataControlDark); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryFooterThemeDataControlDark + .merge(_galleryFooterThemeDataControl), + _galleryFooterThemeDataControl); + }); + + testWidgets( + 'Passing no GalleryFooterThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryFooter( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageFooterTheme = GalleryFooterTheme.of(_context); + expect(imageFooterTheme.backgroundColor, + _galleryFooterThemeDataControl.backgroundColor); + expect(imageFooterTheme.shareIconColor, + _galleryFooterThemeDataControl.shareIconColor); + expect(imageFooterTheme.titleTextStyle, + _galleryFooterThemeDataControl.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, + _galleryFooterThemeDataControl.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, + _galleryFooterThemeDataControl.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, + _galleryFooterThemeDataControl.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, + _galleryFooterThemeDataControl.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, + _galleryFooterThemeDataControl.bottomSheetPhotosTextStyle); + }); + + testWidgets( + 'Passing no GalleryFooterThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryFooter( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageFooterTheme = GalleryFooterTheme.of(_context); + expect(imageFooterTheme.backgroundColor, + _galleryFooterThemeDataControlDark.backgroundColor); + expect(imageFooterTheme.shareIconColor, + _galleryFooterThemeDataControlDark.shareIconColor); + expect(imageFooterTheme.titleTextStyle, + _galleryFooterThemeDataControlDark.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, + _galleryFooterThemeDataControlDark.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, + _galleryFooterThemeDataControlDark.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, + _galleryFooterThemeDataControlDark.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, + _galleryFooterThemeDataControlDark.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, + _galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle); + }); +} + +// Light theme control +final _galleryFooterThemeDataControl = GalleryFooterThemeData( + backgroundColor: ColorTheme.light().barsBg, + shareIconColor: ColorTheme.light().textHighEmphasis, + titleTextStyle: TextTheme.light().headlineBold, + gridIconButtonColor: ColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: ColorTheme.light().barsBg, + bottomSheetBarrierColor: ColorTheme.light().overlay, + bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: TextTheme.light().headlineBold, +); + +// Mid-lerp theme control +const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( + backgroundColor: Color(0xff87898b), + shareIconColor: Color(0xff7f7f7f), + titleTextStyle: TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + gridIconButtonColor: Color(0xff7f7f7f), + bottomSheetBarrierColor: Color(0x4c000000), + bottomSheetBackgroundColor: Color(0xff87898b), + bottomSheetPhotosTextStyle: TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + bottomSheetCloseIconColor: Color(0xff7f7f7f), +); + +// Dark theme control +final _galleryFooterThemeDataControlDark = GalleryFooterThemeData( + backgroundColor: ColorTheme.dark().barsBg, + shareIconColor: ColorTheme.dark().textHighEmphasis, + titleTextStyle: TextTheme.dark().headlineBold, + gridIconButtonColor: ColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: ColorTheme.dark().barsBg, + bottomSheetBarrierColor: ColorTheme.dark().overlay, + bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold, +); diff --git a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart new file mode 100644 index 00000000..cbcc07a6 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockStreamChatClient extends Mock implements StreamChatClient {} + +void main() { + test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { + expect(const GalleryHeaderThemeData(), + const GalleryHeaderThemeData().copyWith()); + expect(const GalleryHeaderThemeData().hashCode, + const GalleryHeaderThemeData().copyWith().hashCode); + }); + + test( + '''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, 1), + _galleryHeaderThemeDataDarkControl); + }); + + test( + '''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, 0.5), + _galleryHeaderThemeDataHalfLerpControl); + }); + + test( + '''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', + () { + expect( + const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, + _galleryHeaderThemeDataControl, 1), + _galleryHeaderThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryHeaderThemeDataControl + .merge(_galleryHeaderThemeDataDarkControl), + _galleryHeaderThemeDataDarkControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _galleryHeaderThemeDataDarkControl + .merge(_galleryHeaderThemeDataControl), + _galleryHeaderThemeDataControl); + }); + + testWidgets( + 'Passing no GalleryHeaderThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryHeader( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageHeaderTheme = GalleryHeaderTheme.of(_context); + expect(imageHeaderTheme.closeButtonColor, + _galleryHeaderThemeDataControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, + _galleryHeaderThemeDataControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, + _galleryHeaderThemeDataControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, + _galleryHeaderThemeDataControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, + _galleryHeaderThemeDataControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, + _galleryHeaderThemeDataControl.bottomSheetBarrierColor); + }); + + testWidgets( + 'Passing no GalleryHeaderThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (context) { + _context = context; + return Scaffold( + appBar: GalleryHeader( + message: Message(), + ), + ); + }, + ), + ), + ); + + final imageHeaderTheme = GalleryHeaderTheme.of(_context); + expect(imageHeaderTheme.closeButtonColor, + _galleryHeaderThemeDataDarkControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, + _galleryHeaderThemeDataDarkControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, + _galleryHeaderThemeDataDarkControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, + _galleryHeaderThemeDataDarkControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, + _galleryHeaderThemeDataDarkControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, + _galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor); + }); +} + +// Light theme test control. +final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xff000000), + backgroundColor: const Color(0xffffffff), + iconMenuPointColor: const Color(0xff000000), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Colors.black, + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2), +); + +// Light theme test control. +final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xff7f7f7f), + backgroundColor: const Color(0xff87898b), + iconMenuPointColor: const Color(0xff7f7f7f), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xff7f7f7f), + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Color(0xff7a7a7a), + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color(0x4c000000), +); + +// Dark theme test control. +final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData( + closeButtonColor: const Color(0xffffffff), + backgroundColor: const Color(0xff101418), + iconMenuPointColor: const Color(0xffffffff), + titleTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + subtitleTextStyle: const TextStyle( + fontSize: 12, + color: Colors.white, + ).copyWith( + color: const Color(0xff7A7A7A), + ), + bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4), +); diff --git a/packages/stream_chat_flutter/test/src/goldens/message_text.png b/packages/stream_chat_flutter/test/src/goldens/message_text.png index dda158b2..ecd93721 100644 Binary files a/packages/stream_chat_flutter/test/src/goldens/message_text.png and b/packages/stream_chat_flutter/test/src/goldens/message_text.png differ diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index e2809d11..0fe3349a 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -37,7 +37,7 @@ void main() { child: WillPopScope( onWillPop: () async => false, child: Scaffold( - body: ImageFooter( + body: GalleryFooter( message: Message(), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index 501d8c0f..f64e2bf1 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -79,7 +79,8 @@ void main() { 'name': 'test', }); - final messageText = '''a message. + const messageText = ''' + a message. with multiple lines and a list: - a. okasd diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index c5183f35..1e3c9f5e 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -47,6 +47,7 @@ void main() { StreamChat( client: client, streamChatThemeData: theme, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: SizedBox( child: ReactionBubble( reactions: [ @@ -60,7 +61,6 @@ void main() { maskColor: theme.ownMessageTheme.reactionsMaskColor!, ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), surfaceSize: const Size(100, 100), ); @@ -83,6 +83,7 @@ void main() { StreamChat( client: client, streamChatThemeData: StreamChatThemeData.fromTheme(themeData), + connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, child: ReactionBubble( @@ -97,7 +98,6 @@ void main() { maskColor: theme.ownMessageTheme.reactionsMaskColor!, ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), surfaceSize: const Size(100, 100), ); diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 6bb1da43..2e221d57 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -89,6 +89,7 @@ void main() { )( StreamChat( client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: StreamChannel( showLoading: false, channel: channel, @@ -100,7 +101,6 @@ void main() { ), ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -141,6 +141,7 @@ void main() { )( StreamChat( client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), child: StreamChannel( showLoading: false, channel: channel, @@ -152,7 +153,6 @@ void main() { ), ), ), - connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 67c80b1e..6c8a3351 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -54,12 +54,12 @@ void main() { ])); when(() => channelState.typingEvents).thenAnswer((i) => { - User(id: 'other-user', extraData: {'name': 'demo'}): + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }); when(() => channelState.typingEventsStream) .thenAnswer((i) => Stream.value({ - User(id: 'other-user', extraData: {'name': 'demo'}): + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), })); diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 4453659e..2f7abfe2 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,27 @@ +## 2.0.0 + +🛑️ Breaking Changes from `1.5.3` + +- migrate this package to null safety +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties + - `options.presence` -> bool presence + +✅ Added + +- Monitor connection using `connectivity_plus` package + +🐞 Fixed + +- Minor fixes +- Performance improvements + +## 2.0.0-nullsafety.9 +- Update llc dependency + ## 2.0.0-nullsafety.8 🛑️ Breaking Changes from `2.0.0-nullsafety.7` diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md index 952caf05..4aaf27d9 100644 --- a/packages/stream_chat_flutter_core/README.md +++ b/packages/stream_chat_flutter_core/README.md @@ -28,7 +28,7 @@ This repo includes a fully functional example app with setup instructions. The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder. ## Add dependency -Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) +Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core) ```yaml dependencies: stream_chat_flutter_core: ^latest_version diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index e7ef400a..4989f38f 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; Future main() async { - /// Create a new instance of [StreamChatClient] passing the apikey obtained from your - /// project dashboard. + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. final client = StreamChatClient('b67pax5b2wdq'); /// Set the current user. In a production scenario, this should be done using @@ -13,12 +13,13 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: { + extraData: const { 'image': 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', }, ), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9' + '.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', ); runApp( @@ -29,36 +30,36 @@ Future main() async { } /// Example application using Stream Chat core widgets. -/// Stream Chat Core is a set of Flutter wrappers which provide basic functionality -/// for building Flutter applications using Stream. +/// Stream Chat Core is a set of Flutter wrappers which provide basic +/// functionality for building Flutter applications using Stream. +/// /// If you'd prefer using pre-made UI widgets for your app, please see our other /// package, `stream_chat_flutter`. class StreamExample extends StatelessWidget { /// Minimal example using Stream's core Flutter package. - /// If you'd prefer using pre-made UI widgets for your app, please see our other - /// package, `stream_chat_flutter`. + /// + /// If you'd prefer using pre-made UI widgets for your app, please see our + /// other package, `stream_chat_flutter`. const StreamExample({ Key? key, required this.client, }) : super(key: key); /// Instance of Stream Client. - /// Stream's [StreamChatClient] can be used to connect to our servers and set the default - /// user for the application. Performing these actions trigger a websocket connection - /// allowing for real-time updates. + /// Stream's [StreamChatClient] can be used to connect to our servers and + /// set the default user for the application. Performing these actions + /// trigger a websocket connection allowing for real-time updates. final StreamChatClient client; @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Stream Chat Core Example', - home: HomeScreen(), - builder: (context, child) => StreamChatCore( - client: client, - child: child!, - ), - ); - } + Widget build(BuildContext context) => MaterialApp( + title: 'Stream Chat Core Example', + home: HomeScreen(), + builder: (context, child) => StreamChatCore( + client: client, + child: child!, + ), + ); } /// Basic layout displaying a list of [Channel]s the user is a part of. @@ -67,104 +68,108 @@ class StreamExample extends StatelessWidget { /// [ChannelListCore] is a `builder` with callbacks for constructing UIs based /// on different scenarios. class HomeScreen extends StatelessWidget { + /// Builds a basic layout displaying a list of [Channel]s the user is a + /// part of. + HomeScreen({Key? key}) : super(key: key); + + /// Controller used for loading more data and controlling pagination in + /// [ChannelListCore]. final channelListController = ChannelListController(); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Channels'), - ), - body: ChannelsBloc( - child: ChannelListCore( - channelListController: channelListController, - filter: Filter.and([ - Filter.equal('type', 'messaging'), - Filter.in_('members', [ - StreamChatCore.of(context).user!.id, - ]) - ]), - emptyBuilder: (BuildContext context) { - return Center( + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Channels'), + ), + body: ChannelsBloc( + child: ChannelListCore( + channelListController: channelListController, + filter: Filter.and([ + Filter.equal('type', 'messaging'), + Filter.in_('members', [ + StreamChatCore.of(context).user!.id, + ]) + ]), + emptyBuilder: (BuildContext context) => const Center( child: Text('Looks like you are not in any channels'), - ); - }, - loadingBuilder: (BuildContext context) { - return Center( + ), + loadingBuilder: (BuildContext context) => const Center( child: SizedBox( - height: 100.0, - width: 100.0, + height: 100, + width: 100, child: CircularProgressIndicator(), ), - ); - }, - errorBuilder: ( - BuildContext context, - dynamic error, - ) { - return Center( + ), + errorBuilder: ( + BuildContext context, + dynamic error, + ) => + Center( child: Text( - 'Oh no, something went wrong. Please check your config. ${error}'), - ); - }, - listBuilder: ( - BuildContext context, - List channels, - ) => - LazyLoadScrollView( - onEndOfPage: () async { - channelListController.paginateData!(); - }, - child: ListView.builder( - itemCount: channels.length, - itemBuilder: (BuildContext context, int index) { - final _item = channels[index]; - return ListTile( - title: Text(_item.name!), - subtitle: StreamBuilder( - stream: _item.state!.lastMessageStream, - initialData: _item.state!.lastMessage, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(snapshot.data!.text!); - } - - return SizedBox(); - }, - ), - onTap: () { - /// Display a list of messages when the user taps on an item. - /// We can use [StreamChannel] to wrap our [MessageScreen] screen - /// with the selected channel. - /// - /// This allows us to use a built-in inherited widget for accessing - /// our `channel` later on. - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: _item, - child: MessageScreen(), - ), - ), - ); - }, - ); + 'Oh no, something went wrong. ' + 'Please check your config. $error', + ), + ), + listBuilder: ( + BuildContext context, + List channels, + ) => + LazyLoadScrollView( + onEndOfPage: () async { + channelListController.paginateData!(); }, + child: ListView.builder( + itemCount: channels.length, + itemBuilder: (BuildContext context, int index) { + final _item = channels[index]; + return ListTile( + title: Text(_item.name!), + subtitle: StreamBuilder( + stream: _item.state!.lastMessageStream, + initialData: _item.state!.lastMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data!.text!); + } + + return const SizedBox(); + }, + ), + onTap: () { + /// Display a list of messages when the user taps on + /// an item. We can use [StreamChannel] to wrap our + /// [MessageScreen] screen with the selected channel. + /// + /// This allows us to use a built-in inherited widget + /// for accessing our `channel` later on. + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: _item, + child: const MessageScreen(), + ), + ), + ); + }, + ); + }, + ), ), ), ), - ), - ); - } + ); } /// A list of messages sent in the current channel. -/// When a user taps on a channel in [HomeScreen], a navigator push [MessageScreen] -/// to display the list of messages in the selected channel. +/// When a user taps on a channel in [HomeScreen], a navigator push +/// [MessageScreen] to display the list of messages in the selected channel. /// /// This is implemented using [MessageListCore], a convenience builder with /// callbacks for building UIs based on different api results. class MessageScreen extends StatefulWidget { + /// Build a MessageScreen + const MessageScreen({Key? key}) : super(key: key); + @override _MessageScreenState createState() => _MessageScreenState(); } @@ -198,19 +203,19 @@ class _MessageScreenState extends State { @override Widget build(BuildContext context) { - /// To access the current channel, we can use the `.of()` method on [StreamChannel] - /// to fetch the closest instance. + /// To access the current channel, we can use the `.of()` method on + /// [StreamChannel] to fetch the closest instance. final channel = StreamChannel.of(context).channel; return Scaffold( appBar: AppBar( - title: StreamBuilder>( - initialData: channel.state?.typingEvents, - stream: channel.state?.typingEventsStream, + title: StreamBuilder>( + initialData: channel.state?.typingEvents.keys, + stream: channel.state?.typingEventsStream.map((it) => it.keys), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!.isNotEmpty) { return Text('${snapshot.data!.first.name} is typing...'); } - return SizedBox(); + return const SizedBox(); }, ), ), @@ -224,57 +229,52 @@ class _MessageScreenState extends State { }, child: MessageListCore( messageListController: messageListController, - emptyBuilder: (BuildContext context) { - return Center( - child: Text('Nothing here yet'), - ); - }, - loadingBuilder: (BuildContext context) { - return Center( - child: SizedBox( - height: 100.0, - width: 100.0, - child: CircularProgressIndicator(), - ), - ); - }, + emptyBuilder: (BuildContext context) => const Center( + child: Text('Nothing here yet'), + ), + loadingBuilder: (BuildContext context) => const Center( + child: SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator(), + ), + ), messageListBuilder: ( BuildContext context, List messages, - ) { - return ListView.builder( - controller: _scrollController, - itemCount: messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = messages[index]; - final client = StreamChatCore.of(context).client; - if (item.user!.id == client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(item.text!), - ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(item.text!), - ), - ); - } - }, - ); - }, - errorWidgetBuilder: (BuildContext context, error) { + ) => + ListView.builder( + controller: _scrollController, + itemCount: messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = messages[index]; + final client = StreamChatCore.of(context).client; + if (item.user!.id == client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text!), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text!), + ), + ); + } + }, + ), + errorBuilder: (BuildContext context, error) { print(error.toString()); - return Center( + return const Center( child: SizedBox( - height: 100.0, - width: 100.0, + height: 100, + width: 100, child: Text('Oh no, an error occured. Please see logs.'), ), @@ -344,7 +344,7 @@ extension on Channel { String? get name { final _channelName = extraData['name']; if (_channelName != null) { - return _channelName; + return _channelName as String; } else { return cid; } diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index 889ad518..6390abc4 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -21,14 +21,14 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 flutter: sdk: flutter stream_chat_flutter_core: path: ../ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.0 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index 6dc52647..c59b2030 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -19,16 +19,15 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// Widget build(BuildContext context) { /// return Scaffold( /// body: ChannelListCore( -/// filter: { -/// 'members': { -/// '\$in': [StreamChat.of(context).user.id], -/// } -/// }, +/// filter: Filter.in_( +/// 'members', +/// [StreamChat.of(context).user!.id], +/// ), /// sort: [SortOption('last_message_at')], /// pagination: PaginationParams( /// limit: 20, /// ), -/// errorBuilder: (err) { +/// errorBuilder: (context, err) { /// return Center( /// child: Text('An error has occured'), /// ); @@ -38,7 +37,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// child: Text('Nothing here...'), /// ); /// }, -/// emptyBuilder: (context) { +/// loadingBuilder: (context) { /// return Center( /// child: CircularProgressIndicator(), /// ); diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 5de9b2ea..46aaccee 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -146,6 +146,8 @@ class ChannelsBlocState extends State _paginationEnded = true; } } catch (e, stk) { + // reset loading controller + _queryChannelsLoadingController.sink.add(false); if (_channelsController.hasValue) { _queryChannelsLoadingController.addError(e, stk); } else { diff --git a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart index 834d00b0..fb503bcd 100644 --- a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart @@ -85,6 +85,7 @@ class _LazyLoadScrollViewState extends State { final extentBefore = notification.metrics.extentBefore; final extentAfter = notification.metrics.extentAfter; final scrollingDown = _scrollPosition < pixels; + _scrollPosition = pixels; if (scrollingDown) { if (extentAfter <= scrollOffset) { @@ -97,8 +98,6 @@ class _LazyLoadScrollViewState extends State { return true; } } - - _scrollPosition = pixels; } if (notification is OverscrollNotification) { if (notification.overscroll > 0) { diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 2a259e83..dc90cc91 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -40,7 +40,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// messageListBuilder: (context, list) { /// return MessagesPage(list); /// }, -/// errorWidgetBuilder: (context, err) { +/// errorBuilder: (context, err) { /// return Center( /// child: Text('Error'), /// ); @@ -67,7 +67,7 @@ class MessageListCore extends StatefulWidget { required this.loadingBuilder, required this.emptyBuilder, required this.messageListBuilder, - required this.errorWidgetBuilder, + required this.errorBuilder, this.parentMessage, this.messageListController, this.messageFilter, @@ -91,7 +91,7 @@ class MessageListCore extends StatefulWidget { /// /// This parameter can be used to display an error message to users in the /// event of a connection failure. - final ErrorBuilder errorWidgetBuilder; + final ErrorBuilder errorBuilder; /// If the current message belongs to a `thread`, this property represents the /// first message or the parent of the conversation. @@ -144,7 +144,7 @@ class MessageListCoreState extends State { growable: false, ), ), - errorBuilder: widget.errorWidgetBuilder, + errorBuilder: widget.errorBuilder, loadingBuilder: widget.loadingBuilder, builder: (context, data) { final messageList = data?.reversed.toList(growable: false) ?? []; diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index daffadfe..44ac9b3f 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -98,6 +98,8 @@ class MessageSearchBlocState extends State _queryMessagesLoadingController.add(false); } } catch (e, stk) { + // reset loading controller + _queryMessagesLoadingController.add(false); if (_messageResponses.hasValue) { _queryMessagesLoadingController.addError(e, stk); } else { diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index d354daec..4901c332 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -17,12 +17,8 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// Widget build(BuildContext context) { /// return Scaffold( /// body: MessageSearchListCore( -/// messageQuery: _channelQuery, -/// filters: { -/// 'members': { -/// r'$in': [user.id] -/// } -/// }, +/// messageQuery: _messageFilter, +/// filters: _channelsFilter, /// paginationParams: PaginationParams(limit: 20), /// ), /// ); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index e80472cd..6e39d5e3 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -159,7 +159,10 @@ class StreamChatCoreState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { - _isInForeground = state == AppLifecycleState.resumed; + _isInForeground = [ + AppLifecycleState.resumed, + AppLifecycleState.inactive, + ].contains(state); if (user != null) { if (_isInForeground) { _onForeground(); diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index af707762..e7916478 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -96,6 +96,8 @@ class UsersBlocState extends State _queryUsersLoadingController.add(false); } } catch (e, stk) { + // reset loading controller + _queryUsersLoadingController.add(false); if (_usersController.hasValue) { _queryUsersLoadingController.addError(e, stk); } else { diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index a231caab..cc59f9fe 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 2.0.0-nullsafety.8 +version: 2.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.0.0-nullsafety.7 + stream_chat: ^2.0.0 dev_dependencies: fake_async: ^1.2.0 diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart index a69f81c9..47aa2bf9 100644 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart @@ -9,26 +9,25 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; void main() { - const pagination = PaginationParams(offset: 0, limit: 3); + const pagination = PaginationParams(limit: 3); List _generateChannels( StreamChatClient client, { int count = 3, int offset = 0, - }) { - return List.generate( - count, - (index) { - index = index + offset; - return Channel( - client, - 'testType$index', - 'testId$index', - extraData: {'extra_data_key': 'extra_data_value_$index'}, - ); - }, - ); - } + }) => + List.generate( + count, + (index) { + index = index + offset; + return Channel( + client, + 'testType$index', + 'testId$index', + extraData: {'extra_data_key': 'extra_data_value_$index'}, + ); + }, + ); testWidgets( 'should throw if ChannelListCore is used where ChannelsBloc is not present ' @@ -37,10 +36,10 @@ void main() { const channelListCoreKey = Key('channelListCore'); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); await tester.pumpWidget(channelListCore); @@ -56,16 +55,16 @@ void main() { const channelListCoreKey = Key('channelListCore'); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -88,10 +87,10 @@ void main() { final controller = ChannelListController(); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), channelListController: controller, ); @@ -101,7 +100,7 @@ void main() { final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -125,9 +124,9 @@ void main() { const errorWidgetKey = Key('errorWidget'); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => Container(key: errorWidgetKey), pagination: pagination, @@ -136,7 +135,7 @@ void main() { final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); const error = 'Error! Error! Error!'; when(() => mockClient.queryChannels( @@ -177,23 +176,23 @@ void main() { ); testWidgets( - 'should build empty widget if channelsBlocState.channelsStream emits empty data', + '''should build empty widget if channelsBlocState.channelsStream emits empty data''', (tester) async { const channelListCoreKey = Key('channelListCore'); const emptyWidgetKey = Key('emptyWidget'); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), pagination: pagination, ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); const channels = []; when(() => mockClient.queryChannels( @@ -234,23 +233,23 @@ void main() { ); testWidgets( - 'should build list widget if channelsBlocState.channelsStream emits some data', + '''should build list widget if channelsBlocState.channelsStream emits some data''', (tester) async { const channelListCoreKey = Key('channelListCore'); const listWidgetKey = Key('listWidget'); final channelListCore = ChannelListCore( key: channelListCoreKey, listBuilder: (_, __) => Container(key: listWidgetKey), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), pagination: pagination, ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); final channels = _generateChannels(mockClient); when(() => mockClient.queryChannels( @@ -298,24 +297,22 @@ void main() { const listWidgetKey = Key('listWidget'); final channelListCore = ChannelListCore( key: channelListCoreKey, - listBuilder: (_, channels) { - return Container( - key: listWidgetKey, - child: Text( - channels.map((e) => e.cid).join(','), - ), - ); - }, - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, channels) => Container( + key: listWidgetKey, + child: Text( + channels.map((e) => e.cid).join(','), + ), + ), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), pagination: pagination, ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); final channels = _generateChannels(mockClient); when(() => mockClient.queryChannels( @@ -413,24 +410,23 @@ void main() { ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore( key: channelListCoreKey, - listBuilder: (_, channels) { - return Container( - key: listWidgetKey, - child: Text( - channels.map((e) => e.cid).join(','), - ), - ); - }, - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, channels) => Container( + key: listWidgetKey, + child: Text( + channels.map((e) => e.cid).join(','), + ), + ), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => + const Offstage(), pagination: pagination.copyWith(limit: limit), ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); final channels = _generateChannels(mockClient); when(() => mockClient.queryChannels( diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index 3c0286a0..a237a23c 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -17,27 +17,26 @@ void main() { StreamChatClient client, { int count = 3, int offset = 0, - }) { - return List.generate( - count, - (index) { - index = index + offset; - return Channel( - client, - 'testType$index', - 'testId$index', - extraData: {'extra_data_key': 'extra_data_value_$index'}, - ); - }, - ); - } + }) => + List.generate( + count, + (index) { + index = index + offset; + return Channel( + client, + 'testType$index', + 'testId$index', + extraData: {'extra_data_key': 'extra_data_value_$index'}, + ); + }, + ); testWidgets( - 'should throw if ChannelsBloc is used where StreamChat is not present in the widget tree', + '''should throw if ChannelsBloc is used where StreamChat is not present in the widget tree''', (tester) async { const channelsBlocKey = Key('channelsBloc'); const childKey = Key('child'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(key: childKey), ); @@ -55,7 +54,7 @@ void main() { (tester) async { const channelsBlocKey = Key('channelsBloc'); const childKey = Key('child'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(key: childKey), ); @@ -63,7 +62,7 @@ void main() { final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -86,16 +85,14 @@ void main() { key: channelsBlocKey, child: Builder( key: childKey, - builder: (context) { - return Offstage(); - }, + builder: (context) => const Offstage(), ), ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -157,16 +154,14 @@ void main() { key: channelsBlocKey, child: Builder( key: childKey, - builder: (context) { - return Offstage(); - }, + builder: (context) => const Offstage(), ), ); final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -218,7 +213,7 @@ void main() { 'through queryChannelsLoading', (tester) async { const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(), ); @@ -226,7 +221,7 @@ void main() { final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -323,7 +318,7 @@ void main() { 'client.queryChannels() throws', (tester) async { const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(), ); @@ -331,7 +326,7 @@ void main() { final mockClient = MockClient(); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); await tester.pumpWidget( StreamChatCore( @@ -345,7 +340,7 @@ void main() { ); final channels = _generateChannels(mockClient); - final paginationParams = const PaginationParams( + const paginationParams = PaginationParams( limit: 3, ); @@ -380,7 +375,7 @@ void main() { paginationParams: paginationParams, )).called(1); - final error = 'Error! Error! Error!'; + const error = 'Error! Error! Error!'; when(() => mockClient.queryChannels( filter: any(named: 'filter'), @@ -424,13 +419,13 @@ void main() { (tester) async { final mockClient = MockClient(); const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(), ); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.channelHidden, @@ -503,13 +498,13 @@ void main() { (tester) async { final mockClient = MockClient(); const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(), ); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.channelDeleted, @@ -589,18 +584,18 @@ void main() { ); testWidgets( - 'event channel should be moved to top of the list if present when' + 'event channel should be moved to top of the list if present when ' 'EventType.messageNew event is received', (tester) async { final mockClient = MockClient(); const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( + const channelsBloc = ChannelsBloc( key: channelsBlocKey, child: Offstage(), ); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.messageNew, @@ -678,21 +673,19 @@ void main() { (tester) async { final hiddenChannelEventController = StreamController(); - addTearDown(() { - hiddenChannelEventController.close(); - }); + addTearDown(hiddenChannelEventController.close); final mockClient = MockClient(); final channels = _generateChannels(mockClient); const channelsBlocKey = Key('channelsBloc'); final channelsBloc = ChannelsBloc( key: channelsBlocKey, - child: Offstage(), shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid), + child: const Offstage(), ); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.channelHidden, @@ -789,14 +782,14 @@ void main() { const channelsBlocKey = Key('channelsBloc'); final channelsBloc = ChannelsBloc( key: channelsBlocKey, - child: Offstage(), shouldAddChannel: (_) => true, + child: const Offstage(), ); when(() => mockClient.state.channels).thenReturn(stateChannels); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.messageNew, @@ -868,21 +861,21 @@ void main() { final mockClient = MockClient(); final channels = _generateChannels(mockClient); int channelComparator(Channel a, Channel b) { - final aData = a.extraData['extra_data_key'] as String; - final bData = b.extraData['extra_data_key'] as String; + final aData = a.extraData['extra_data_key'].toString(); + final bData = b.extraData['extra_data_key'].toString(); return bData.compareTo(aData); } const channelsBlocKey = Key('channelsBloc'); final channelsBloc = ChannelsBloc( key: channelsBlocKey, - child: Offstage(), shouldAddChannel: (_) => true, channelsComparator: channelComparator, + child: const Offstage(), ); when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => Stream.empty()); + .thenAnswer((_) => const Stream.empty()); when(() => mockClient.on( EventType.messageNew, diff --git a/packages/stream_chat_flutter_core/test/lazy_load_scroll_view_test.dart b/packages/stream_chat_flutter_core/test/lazy_load_scroll_view_test.dart index af59088f..86b39e23 100644 --- a/packages/stream_chat_flutter_core/test/lazy_load_scroll_view_test.dart +++ b/packages/stream_chat_flutter_core/test/lazy_load_scroll_view_test.dart @@ -7,7 +7,7 @@ void main() { 'should render LazyLoadScrollView if child is provided', (tester) async { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); - final lazyLoadScrollView = LazyLoadScrollView( + const lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, child: Offstage(), ); @@ -23,7 +23,7 @@ void main() { (tester) async { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childKey = Key('childKey'); - final lazyLoadScrollView = LazyLoadScrollView( + const lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, child: Offstage(key: childKey), ); @@ -41,7 +41,7 @@ void main() { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childListViewKey = Key('childListView'); - bool onPageScrollStartCalled = false; + var onPageScrollStartCalled = false; final lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, @@ -52,7 +52,7 @@ void main() { key: childListViewKey, children: List.generate( 12, - (index) => Container( + (index) => SizedBox( height: 100, child: Text('Item #$index'), ), @@ -71,7 +71,7 @@ void main() { expect(find.byKey(childListViewKey), findsOneWidget); expect(onPageScrollStartCalled, isFalse); - await tester.startGesture(const Offset(100.0, 100.0)); + await tester.startGesture(const Offset(100, 100)); await tester.pump(const Duration(seconds: 1)); expect(onPageScrollStartCalled, isTrue); @@ -85,8 +85,8 @@ void main() { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childListViewKey = Key('childListView'); - bool onPageScrollStartCalled = false; - bool onPageScrollEndCalled = false; + var onPageScrollStartCalled = false; + var onPageScrollEndCalled = false; final lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, @@ -100,7 +100,7 @@ void main() { key: childListViewKey, children: List.generate( 12, - (index) => Container( + (index) => SizedBox( height: 100, child: Text('Item #$index'), ), @@ -122,7 +122,7 @@ void main() { final gesture = await tester.createGesture(); - await gesture.down(const Offset(100.0, 100.0)); + await gesture.down(const Offset(100, 100)); await tester.pump(const Duration(seconds: 1)); expect(onPageScrollStartCalled, isTrue); @@ -141,7 +141,7 @@ void main() { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childListViewKey = Key('childListView'); - bool onInBetweenOfPageCalled = false; + var onInBetweenOfPageCalled = false; final lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, @@ -152,7 +152,7 @@ void main() { key: childListViewKey, children: List.generate( 12, - (index) => Container( + (index) => SizedBox( height: 100, child: Text('Item #$index'), ), @@ -173,9 +173,9 @@ void main() { final gesture = await tester.createGesture(); - await gesture.down(const Offset(100.0, 100.0)); + await gesture.down(const Offset(100, 100)); await tester.pump(const Duration(seconds: 1)); - await gesture.moveBy(const Offset(-200.0, -200.0)); + await gesture.moveBy(const Offset(-200, -200)); await tester.pump(const Duration(seconds: 1)); expect(onInBetweenOfPageCalled, isTrue); @@ -189,7 +189,7 @@ void main() { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childListViewKey = Key('childListView'); - bool onStartOfPageCalled = false; + var onStartOfPageCalled = false; final lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, @@ -200,7 +200,7 @@ void main() { key: childListViewKey, children: List.generate( 12, - (index) => Container( + (index) => SizedBox( height: 100, child: Text('Item #$index'), ), @@ -221,11 +221,11 @@ void main() { final gesture = await tester.createGesture(); - await gesture.down(const Offset(100.0, 100.0)); + await gesture.down(const Offset(100, 100)); await tester.pump(const Duration(seconds: 1)); - await gesture.moveBy(const Offset(-200.0, -200.0)); + await gesture.moveBy(const Offset(-200, -200)); await tester.pump(const Duration(seconds: 1)); - await gesture.moveBy(const Offset(201.0, 201.0)); + await gesture.moveBy(const Offset(201, 201)); await tester.pump(const Duration(seconds: 1)); expect(onStartOfPageCalled, isTrue); @@ -239,7 +239,7 @@ void main() { const lazyLoadScrollViewKey = Key('lazyLoadScrollView'); const childListViewKey = Key('childListView'); - bool onEndOfPageCalled = false; + var onEndOfPageCalled = false; final lazyLoadScrollView = LazyLoadScrollView( key: lazyLoadScrollViewKey, @@ -250,7 +250,7 @@ void main() { key: childListViewKey, children: List.generate( 12, - (index) => Container( + (index) => SizedBox( height: 100, child: Text('Item #$index'), ), @@ -271,9 +271,9 @@ void main() { final gesture = await tester.createGesture(); - await gesture.down(const Offset(100.0, 100.0)); + await gesture.down(const Offset(100, 100)); await tester.pump(const Duration(seconds: 1)); - await gesture.moveBy(const Offset(-601.0, -601.0)); + await gesture.moveBy(const Offset(-601, -601)); await tester.pump(const Duration(seconds: 1)); expect(onEndOfPageCalled, isTrue); diff --git a/packages/stream_chat_flutter_core/test/matchers/channel_matcher.dart b/packages/stream_chat_flutter_core/test/matchers/channel_matcher.dart index f6d7fb50..514462e0 100644 --- a/packages/stream_chat_flutter_core/test/matchers/channel_matcher.dart +++ b/packages/stream_chat_flutter_core/test/matchers/channel_matcher.dart @@ -1,4 +1,3 @@ -import 'package:meta/meta.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -8,7 +7,7 @@ Matcher isSameChannelAs(Channel targetChannel) => class _IsSameChannelAs extends Matcher { const _IsSameChannelAs({ required this.targetChannel, - }) : assert(targetChannel != null, ''); + }); final Channel targetChannel; @@ -27,13 +26,13 @@ Matcher isSameChannelListAs(List targetChannelList) => class _IsSameChannelListAs extends Matcher { const _IsSameChannelListAs({ required this.targetChannelList, - }) : assert(targetChannelList != null, ''); + }); final List targetChannelList; @override bool matches(covariant List channelList, Map matchState) { - bool matches = true; + var matches = true; for (var i = 0; i < channelList.length; i++) { final channel = channelList[i]; final targetChannel = targetChannelList[i]; diff --git a/packages/stream_chat_flutter_core/test/matchers/message_matcher.dart b/packages/stream_chat_flutter_core/test/matchers/message_matcher.dart index 69a24f49..776665ff 100644 --- a/packages/stream_chat_flutter_core/test/matchers/message_matcher.dart +++ b/packages/stream_chat_flutter_core/test/matchers/message_matcher.dart @@ -1,4 +1,3 @@ -import 'package:meta/meta.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -8,7 +7,7 @@ Matcher isSameMessageAs(Message targetMessage) => class _IsSameMessageAs extends Matcher { const _IsSameMessageAs({ required this.targetMessage, - }) : assert(targetMessage != null, ''); + }); final Message targetMessage; @@ -27,13 +26,13 @@ Matcher isSameMessageListAs(List targetMessageList) => class _IsSameMessageListAs extends Matcher { const _IsSameMessageListAs({ required this.targetMessageList, - }) : assert(targetMessageList != null, ''); + }); final List targetMessageList; @override bool matches(covariant List messageList, Map matchState) { - bool matches = true; + var matches = true; for (var i = 0; i < messageList.length; i++) { final message = messageList[i]; final targetMessage = targetMessageList[i]; diff --git a/packages/stream_chat_flutter_core/test/matchers/users_matcher.dart b/packages/stream_chat_flutter_core/test/matchers/users_matcher.dart index 38e25a64..fa0f0c08 100644 --- a/packages/stream_chat_flutter_core/test/matchers/users_matcher.dart +++ b/packages/stream_chat_flutter_core/test/matchers/users_matcher.dart @@ -1,4 +1,3 @@ -import 'package:meta/meta.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -7,7 +6,7 @@ Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser); class _IsSameUserAs extends Matcher { const _IsSameUserAs({ required this.targetUser, - }) : assert(targetUser != null, ''); + }); final User targetUser; @@ -25,13 +24,13 @@ Matcher isSameUserListAs(List targetUserList) => class _IsSameUserListAs extends Matcher { const _IsSameUserListAs({ required this.targetUserList, - }) : assert(targetUserList != null, ''); + }); final List targetUserList; @override bool matches(covariant List userList, Map matchState) { - bool matches = true; + var matches = true; for (var i = 0; i < userList.length; i++) { final user = userList[i]; final targetUser = targetUserList[i]; diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index 98e83f9f..b038411b 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -27,10 +27,9 @@ void main() { type: 'testType', user: users[index], createdAt: DateTime.now(), - shadowed: false, replyCount: index, updatedAt: DateTime.now(), - extraData: {'extra_test_field': 'extraTestData'}, + extraData: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: true, pinnedAt: DateTime.now(), @@ -48,10 +47,9 @@ void main() { user: users[index], parentId: messages[0].id, createdAt: DateTime.now(), - shadowed: false, replyCount: index, updatedAt: DateTime.now(), - extraData: {'extra_test_field': 'extraTestData'}, + extraData: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: true, pinnedAt: DateTime.now(), @@ -63,16 +61,15 @@ void main() { } testWidgets( - 'should throw if MessageListCore is used where StreamChannel is not present ' - 'in the widget tree', + '''should throw if MessageListCore is used where StreamChannel is not present in the widget tree''', (tester) async { const messageListCoreKey = Key('messageListCore'); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); await tester.pumpWidget(messageListCore); @@ -88,10 +85,10 @@ void main() { const messageListCoreKey = Key('messageListCore'); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockChannel = MockChannel(); @@ -120,10 +117,10 @@ void main() { final controller = MessageListController(); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), messageListController: controller, ); @@ -150,16 +147,16 @@ void main() { ); testWidgets( - 'should assign paginateData callback and paginate data correctly if a MessageListController is passed', + '''should assign paginateData callback and paginate data correctly if a MessageListController is passed''', (tester) async { const messageListCoreKey = Key('messageListCore'); final controller = MessageListController(); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), messageListController: controller, ); @@ -207,10 +204,10 @@ void main() { const errorWidgetKey = Key('errorWidget'); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage( + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage( key: errorWidgetKey, ), ); @@ -249,10 +246,11 @@ void main() { const emptyWidgetKey = Key('emptyWidget'); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => + const Offstage(key: emptyWidgetKey), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockChannel = MockChannel(); @@ -289,10 +287,10 @@ void main() { const listWidgetKey = Key('listWidget'); final messageListCore = MessageListCore( key: messageListCoreKey, - messageListBuilder: (_, __) => Offstage(key: listWidgetKey), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + messageListBuilder: (_, __) => const Offstage(key: listWidgetKey), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockChannel = MockChannel(); @@ -344,9 +342,9 @@ void main() { messages.reversed.map((it) => it.id).join(','), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockChannel = MockChannel(); @@ -391,9 +389,9 @@ void main() { messages.reversed.map((it) => '${it.parentId}-${it.id}').join(','), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorWidgetBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), parentMessage: parentMessage, ); diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 4d67ebd9..979ca5b2 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -8,34 +8,33 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'matchers/get_message_response_matcher.dart'; import 'mocks.dart'; -final testFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); void main() { List _generateMessages({ int count = 3, int offset = 0, - }) { - return List.generate( - count, - (index) { - index = index + offset; - return GetMessageResponse() - ..message = Message( - id: 'testId$index', - text: 'testTextData$index', - ) - ..channel = ChannelModel( - cid: 'testCid:id', - ); - }, - ); - } + }) => + List.generate( + count, + (index) { + index = index + offset; + return GetMessageResponse() + ..message = Message( + id: 'testId$index', + text: 'testTextData$index', + ) + ..channel = ChannelModel( + cid: 'testCid:id', + ); + }, + ); testWidgets( 'messageSearchBlocState.search() should throw if used where ' 'StreamChat is not present in the widget tree', (tester) async { - final messageSearchBloc = MessageSearchBloc( + const messageSearchBloc = MessageSearchBloc( child: Offstage(), ); @@ -49,7 +48,7 @@ void main() { (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); - final messageSearchBloc = MessageSearchBloc( + const messageSearchBloc = MessageSearchBloc( key: messageSearchBlocKey, child: Offstage(key: childKey), ); @@ -101,7 +100,7 @@ void main() { (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); - final messageSearchBloc = MessageSearchBloc( + const messageSearchBloc = MessageSearchBloc( key: messageSearchBlocKey, child: Offstage(key: childKey), ); @@ -151,7 +150,7 @@ void main() { (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); - final messageSearchBloc = MessageSearchBloc( + const messageSearchBloc = MessageSearchBloc( key: messageSearchBlocKey, child: Offstage(key: childKey), ); @@ -243,7 +242,7 @@ void main() { (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); - final messageSearchBloc = MessageSearchBloc( + const messageSearchBloc = MessageSearchBloc( key: messageSearchBlocKey, child: Offstage(key: childKey), ); diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index 932e35b4..6b700c76 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -6,7 +6,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; -final testFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); void main() { List _generateMessages({ @@ -56,10 +56,10 @@ void main() { const messageSearchListCoreKey = Key('messageSearchListCore'); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, - childBuilder: (List messages) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object? error) => Offstage(), + childBuilder: (List messages) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, ); @@ -86,10 +86,10 @@ void main() { final controller = MessageSearchListController(); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, - childBuilder: (List messages) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + childBuilder: (List messages) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), messageSearchListController: controller, filters: testFilter, ); @@ -122,10 +122,10 @@ void main() { const errorWidgetKey = Key('errorWidget'); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, - childBuilder: (List messages) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage( + childBuilder: (List messages) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage( key: errorWidgetKey, ), filters: testFilter, @@ -173,10 +173,11 @@ void main() { const emptyWidgetKey = Key('emptyWidget'); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, - childBuilder: (List messages) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => Offstage(), + childBuilder: (List messages) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => + const Offstage(key: emptyWidgetKey), + errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, ); @@ -223,12 +224,12 @@ void main() { const childWidgetKey = Key('childWidget'); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, - childBuilder: (List messages) => Offstage( + childBuilder: (List messages) => const Offstage( key: childWidgetKey, ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, ); @@ -283,9 +284,9 @@ void main() { messages.map((e) => '${e.channel?.cid}-${e.message.id}').join(','), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), paginationParams: pagination, filters: testFilter, ); @@ -399,9 +400,10 @@ void main() { .join(','), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => + const Offstage(), paginationParams: pagination.copyWith(limit: limit), filters: testFilter, ); diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index 2b32cf2d..08763f9d 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -27,10 +27,9 @@ void main() { type: 'testType', user: users[index], createdAt: DateTime.now(), - shadowed: false, replyCount: index, updatedAt: DateTime.now(), - extraData: {'extra_test_field': 'extraTestData'}, + extraData: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: true, pinnedAt: DateTime.now(), @@ -48,10 +47,9 @@ void main() { user: users[index], parentId: messages[0].id, createdAt: DateTime.now(), - shadowed: false, replyCount: index, updatedAt: DateTime.now(), - extraData: {'extra_test_field': 'extraTestData'}, + extraData: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: true, pinnedAt: DateTime.now(), @@ -72,7 +70,7 @@ void main() { final streamChannel = StreamChannel( key: streamChannelKey, channel: mockChannel, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChannel); @@ -91,10 +89,10 @@ void main() { final streamChannel = StreamChannel( key: streamChannelKey, channel: mockChannel, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); - final errorMessage = 'Error! Error! Error!'; + const errorMessage = 'Error! Error! Error!'; final error = DioError( type: DioErrorType.response, error: errorMessage, @@ -128,8 +126,7 @@ void main() { final streamChannel = StreamChannel( key: streamChannelKey, channel: mockChannel, - child: Offstage(key: childKey), - showLoading: true, + child: const Offstage(key: childKey), ); when(() => mockChannel.initialized).thenAnswer((_) async => false); @@ -158,8 +155,8 @@ void main() { final streamChannel = StreamChannel( key: streamChannelKey, channel: mockChannel, - child: Offstage(key: childKey), initialMessageId: 'testInitialMessageId', + child: const Offstage(key: childKey), ); when(() => mockChannel.initialized).thenAnswer((_) async => true); @@ -213,8 +210,8 @@ void main() { StreamChannel( key: streamChannelKey, channel: mockChannel, - child: Offstage(key: childKey), initialMessageId: initialMessageId, + child: const Offstage(key: childKey), ); final beforePagination = PaginationParams( diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index f2b1b514..ee9a86cb 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -23,7 +23,7 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -42,7 +42,7 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -63,7 +63,7 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -71,7 +71,8 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - when(() => mockClient.closeConnection()).thenAnswer((_) async { + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { return; }); @@ -79,9 +80,10 @@ void main() { find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused); - verify(() => mockClient.closeConnection()).called(1); + verify(mockClient.closeConnection).called(1); }, ); @@ -93,16 +95,16 @@ void main() { await tester.runAsync(() async { final mockClient = MockClient(); final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived(); - const backgroundKeepAlive = const Duration(seconds: 3); + const backgroundKeepAlive = Duration(seconds: 3); const streamChatCoreKey = Key('streamChatCore'); const childKey = Key('child'); final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), onBackgroundEventReceived: mockOnBackgroundEventReceived, backgroundKeepAlive: backgroundKeepAlive, connectivityStream: Stream.value(ConnectivityResult.mobile), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -111,8 +113,9 @@ void main() { expect(find.byKey(childKey), findsOneWidget); final event = Event(type: EventType.any); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.closeConnection()).thenAnswer((_) async { + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { return; }); @@ -120,6 +123,7 @@ void main() { find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.paused); @@ -129,7 +133,7 @@ void main() { await Future.delayed(backgroundKeepAlive); - verify(() => mockClient.closeConnection()).called(1); + verify(mockClient.closeConnection).called(1); verifyNever(() => mockOnBackgroundEventReceived.call(event)); }); }, @@ -143,15 +147,15 @@ void main() { await tester.runAsync(() async { final mockClient = MockClient(); final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived(); - const backgroundKeepAlive = const Duration(seconds: 3); + const backgroundKeepAlive = Duration(seconds: 3); const streamChatCoreKey = Key('streamChatCore'); const childKey = Key('child'); final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), onBackgroundEventReceived: mockOnBackgroundEventReceived, backgroundKeepAlive: backgroundKeepAlive, + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -160,12 +164,13 @@ void main() { expect(find.byKey(childKey), findsOneWidget); final event = Event(type: EventType.any); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); final streamChatCoreState = tester.state( find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.paused); @@ -193,8 +198,8 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), connectivityStream: Stream.value(ConnectivityResult.mobile), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -203,9 +208,14 @@ void main() { expect(find.byKey(childKey), findsOneWidget); final event = Event(type: EventType.any); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); - when(() => mockClient.closeConnection()).thenAnswer((_) async => null); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { + return; + }); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -213,6 +223,7 @@ void main() { find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.paused); @@ -221,7 +232,7 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verify(() => mockClient.openConnection()).called(1); + verify(mockClient.openConnection).called(1); }); }, ); @@ -237,17 +248,22 @@ void main() { const childKey = Key('child'); final event = Event(); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); - when(() => mockClient.closeConnection()).thenAnswer((_) async => null); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { + return; + }); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), connectivityStream: Stream.value(ConnectivityResult.none), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -259,6 +275,7 @@ void main() { find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.paused); @@ -267,7 +284,148 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verifyNever(() => mockClient.openConnection()); + verifyNever(mockClient.openConnection); + }); + }, + ); + testWidgets( + 'didChangeAppLifecycleState should cancel the backgroundKeepAlive timer ' + 'if it is currently running in case the widget lifestyle changes to ' + 'AppLifecycleState.inactive', + (tester) async { + await tester.runAsync(() async { + final mockClient = MockClient(); + final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived(); + const backgroundKeepAlive = Duration(seconds: 3); + const streamChatCoreKey = Key('streamChatCore'); + const childKey = Key('child'); + final streamChatCore = StreamChatCore( + key: streamChatCoreKey, + client: mockClient, + onBackgroundEventReceived: mockOnBackgroundEventReceived, + backgroundKeepAlive: backgroundKeepAlive, + child: const Offstage(key: childKey), + ); + + await tester.pumpWidget(streamChatCore); + + expect(find.byKey(streamChatCoreKey), findsOneWidget); + expect(find.byKey(childKey), findsOneWidget); + + final event = Event(type: EventType.any); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + + final streamChatCoreState = tester.state( + find.byKey(streamChatCoreKey), + ); + + // ignore: cascade_invocations + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.paused); + + await untilCalled(() => mockOnBackgroundEventReceived.call(event)); + + verify(() => mockOnBackgroundEventReceived.call(event)).called(1); + + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.inactive); + + verifyNever(() => mockOnBackgroundEventReceived.call(event)); + }); + }, + ); + + testWidgets( + 'didChangeAppLifecycleState should call client.connect() ' + 'if the connectionStatus is ConnectionStatus.disconnected in case the ' + 'widget lifestyle changes to AppLifecycleState.inactive', + (tester) async { + await tester.runAsync(() async { + final mockClient = MockClient(); + const streamChatCoreKey = Key('streamChatCore'); + const childKey = Key('child'); + final streamChatCore = StreamChatCore( + key: streamChatCoreKey, + client: mockClient, + connectivityStream: Stream.value(ConnectivityResult.mobile), + child: const Offstage(key: childKey), + ); + + await tester.pumpWidget(streamChatCore); + + expect(find.byKey(streamChatCoreKey), findsOneWidget); + expect(find.byKey(childKey), findsOneWidget); + + final event = Event(type: EventType.any); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + when(mockClient.closeConnection).thenAnswer((_) async {}); + when(() => mockClient.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); + + final streamChatCoreState = tester.state( + find.byKey(streamChatCoreKey), + ); + + // ignore: cascade_invocations + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.paused); + + await Future.delayed(const Duration(seconds: 1)); + + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.inactive); + + verify(mockClient.openConnection).called(1); + }); + }, + ); + + testWidgets( + 'didChangeAppLifecycleState should not call client.openConnection() ' + 'if connection is not available in case the ' + 'widget lifestyle changes to AppLifecycleState.inactive', + (tester) async { + await tester.runAsync(() async { + final mockClient = MockClient(); + const streamChatCoreKey = Key('streamChatCore'); + const childKey = Key('child'); + + final event = Event(); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + when(mockClient.closeConnection).thenAnswer((_) async {}); + when(() => mockClient.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); + + final streamChatCore = StreamChatCore( + key: streamChatCoreKey, + client: mockClient, + connectivityStream: Stream.value(ConnectivityResult.none), + child: const Offstage(key: childKey), + ); + + await tester.pumpWidget(streamChatCore); + + expect(find.byKey(streamChatCoreKey), findsOneWidget); + expect(find.byKey(childKey), findsOneWidget); + + final streamChatCoreState = tester.state( + find.byKey(streamChatCoreKey), + ); + + // ignore: cascade_invocations + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.paused); + + await Future.delayed(const Duration(seconds: 1)); + + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.inactive); + + verifyNever(mockClient.openConnection); }); }, ); @@ -285,7 +443,7 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -308,9 +466,7 @@ void main() { emits(ownUser), ); - addTearDown(() { - userController.close(); - }); + addTearDown(userController.close); }); }, ); @@ -326,17 +482,22 @@ void main() { BehaviorSubject.seeded(ConnectivityResult.none); final event = Event(); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); - when(() => mockClient.closeConnection()).thenAnswer((_) async => null); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { + return; + }); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), connectivityStream: _connectivityController.stream, + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -348,7 +509,9 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.openConnection()).called(1); + verify(mockClient.openConnection).called(1); + + addTearDown(_connectivityController.close); }); }, ); @@ -365,8 +528,8 @@ void main() { final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), connectivityStream: _connectivityController.stream, + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -375,9 +538,14 @@ void main() { expect(find.byKey(childKey), findsOneWidget); final event = Event(); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); - when(() => mockClient.closeConnection()).thenAnswer((_) async => null); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { + return; + }); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.connected); @@ -385,7 +553,9 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.closeConnection()).called(1); + verify(mockClient.closeConnection).called(1); + + addTearDown(_connectivityController.close); }); }, ); @@ -401,17 +571,22 @@ void main() { BehaviorSubject.seeded(ConnectivityResult.none); final event = Event(); - when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); - when(() => mockClient.closeConnection()).thenAnswer((_) async => null); + when(mockClient.on).thenAnswer((_) => Stream.value(event)); + when(mockClient.openConnection) + .thenAnswer((_) async => OwnUser(id: 'test')); + + // ignore: prefer_expression_function_bodies + when(mockClient.closeConnection).thenAnswer((_) async { + return; + }); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, - child: Offstage(key: childKey), connectivityStream: _connectivityController.stream, + child: const Offstage(key: childKey), ); await tester.pumpWidget(streamChatCore); @@ -423,6 +598,7 @@ void main() { find.byKey(streamChatCoreKey), ); + // ignore: cascade_invocations streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.paused); @@ -432,7 +608,9 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verifyNever(() => mockClient.closeConnection()); + verifyNever(mockClient.closeConnection); + + addTearDown(_connectivityController.close); }); }, ); diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index 95077e5c..eac8dc71 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -12,26 +12,24 @@ void main() { List _generateUsers({ int count = 3, int offset = 0, - }) { - return List.generate( - count, - (index) { - index = index + offset; - return User( - id: 'testId$index', - role: 'testRole$index', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - lastActive: DateTime.now(), - online: true, - banned: false, - extraData: { - 'name': '${alphabets[index]}-testName', - }, - ); - }, - ); - } + }) => + List.generate( + count, + (index) { + index = index + offset; + return User( + id: 'testId$index', + role: 'testRole$index', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: true, + extraData: { + 'name': '${alphabets[index]}-testName', + }, + ); + }, + ); testWidgets( 'should throw if UserListCore is used where UsersBloc is not present ' @@ -40,10 +38,10 @@ void main() { const userListCoreKey = Key('userListCore'); final userListCore = UserListCore( key: userListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); await tester.pumpWidget(userListCore); @@ -59,10 +57,10 @@ void main() { const userListCoreKey = Key('userListCore'); final userListCore = UserListCore( key: userListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockClient = MockClient(); @@ -88,10 +86,10 @@ void main() { final controller = UserListController(); final userListCore = UserListCore( key: userListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), userListController: controller, ); @@ -122,9 +120,9 @@ void main() { const errorWidgetKey = Key('errorWidget'); final userListCore = UserListCore( key: userListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => Container(key: errorWidgetKey), ); @@ -168,10 +166,10 @@ void main() { const emptyWidgetKey = Key('emptyWidget'); final userListCore = UserListCore( key: userListCoreKey, - listBuilder: (_, __) => Offstage(), - loadingBuilder: (BuildContext context) => Offstage(), + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockClient = MockClient(); @@ -214,9 +212,9 @@ void main() { final userListCore = UserListCore( key: userListCoreKey, listBuilder: (_, __) => Container(key: listWidgetKey), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), ); final mockClient = MockClient(); @@ -261,20 +259,20 @@ void main() { listBuilder: (_, items) => Container( key: listWidgetKey, child: ListView( - children: items.map((e) { - return Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: (heading) => Text(heading), - userItem: (user) => Text(user.id), - ), - ); - }).toList(growable: false), + children: items + .map((e) => Container( + key: Key(e.key ?? ''), + child: e.when( + headerItem: (heading) => Text(heading), + userItem: (user) => Text(user.id), + ), + )) + .toList(growable: false), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), groupAlphabetically: true, ); @@ -329,20 +327,20 @@ void main() { listBuilder: (_, items) => Container( key: listWidgetKey, child: ListView( - children: items.map((e) { - return Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: (heading) => Text(heading), - userItem: (user) => Text(user.id), - ), - ); - }).toList(growable: false), + children: items + .map((e) => Container( + key: Key(e.key ?? ''), + child: e.when( + headerItem: (heading) => Text(heading), + userItem: (user) => Text(user.id), + ), + )) + .toList(growable: false), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), pagination: pagination, groupAlphabetically: true, ); @@ -425,7 +423,7 @@ void main() { const pagination = PaginationParams(); StateSetter? _stateSetter; - int limit = pagination.limit; + var limit = pagination.limit; const userListCoreKey = Key('userListCore'); const listWidgetKey = Key('listWidget'); @@ -434,20 +432,21 @@ void main() { listBuilder: (_, items) => Container( key: listWidgetKey, child: ListView( - children: items.map((e) { - return Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: (heading) => Text(heading), - userItem: (user) => Text(user.id), - ), - ); - }).toList(growable: false), + children: items + .map((e) => Container( + key: Key(e.key ?? ''), + child: e.when( + headerItem: (heading) => Text(heading), + userItem: (user) => Text(user.id), + ), + )) + .toList(growable: false), ), ), - loadingBuilder: (BuildContext context) => Offstage(), - emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (BuildContext context, Object error) => Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => + const Offstage(), pagination: pagination.copyWith(limit: limit), groupAlphabetically: true, ); diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index b659ca34..af088a76 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -12,30 +12,28 @@ void main() { List _generateUsers({ int count = 3, int offset = 0, - }) { - return List.generate( - count, - (index) { - index = index + offset; - return User( - id: 'testId$index', - role: 'testRole$index', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - lastActive: DateTime.now(), - online: true, - banned: false, - extraData: {'extra_data_key': 'extraDataValue'}, - ); - }, - ); - } + }) => + List.generate( + count, + (index) { + index = index + offset; + return User( + id: 'testId$index', + role: 'testRole$index', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + lastActive: DateTime.now(), + online: true, + extraData: const {'extra_data_key': 'extraDataValue'}, + ); + }, + ); testWidgets( 'usersBlocState.queryUsers() should throw if used where ' 'StreamChat is not present in the widget tree', (tester) async { - final usersBloc = UsersBloc( + const usersBloc = UsersBloc( child: Offstage(), ); @@ -49,7 +47,7 @@ void main() { (tester) async { const usersBlocKey = Key('usersBloc'); const childKey = Key('child'); - final usersBloc = UsersBloc( + const usersBloc = UsersBloc( key: usersBlocKey, child: Offstage(key: childKey), ); @@ -98,7 +96,7 @@ void main() { (tester) async { const usersBlocKey = Key('usersBloc'); const childKey = Key('child'); - final usersBloc = UsersBloc( + const usersBloc = UsersBloc( key: usersBlocKey, child: Offstage(key: childKey), ); @@ -116,7 +114,7 @@ void main() { find.byKey(usersBlocKey), ); - final error = 'Error! Error! Error!'; + const error = 'Error! Error! Error!'; when(() => mockClient.queryUsers( filter: any(named: 'filter'), @@ -148,7 +146,7 @@ void main() { (tester) async { const usersBlocKey = Key('usersBloc'); const childKey = Key('child'); - final usersBloc = UsersBloc( + const usersBloc = UsersBloc( key: usersBlocKey, child: Offstage(key: childKey), ); @@ -231,7 +229,7 @@ void main() { (tester) async { const usersBlocKey = Key('usersBloc'); const childKey = Key('child'); - final usersBloc = UsersBloc( + const usersBloc = UsersBloc( key: usersBlocKey, child: Offstage(key: childKey), ); @@ -275,7 +273,7 @@ void main() { final offset = users.length; final pagination = PaginationParams(offset: offset); - final error = 'Error! Error! Error!'; + const error = 'Error! Error! Error!'; when(() => mockClient.queryUsers( filter: any(named: 'filter'), diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 38742b1e..3a65db58 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 +* Migrate this package to null safety +* Minor fixes and improvements + +## 2.0.0-nullsafety.8 +* Updated llc dependency +* Upgraded moor dependencies and generated files with the latest dependency + ## 2.0.0-nullsafety.7 * Update llc dependency diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index f61bfed0..ab6dcd1c 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -3,8 +3,8 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; Future main() async { - /// Create a new instance of [StreamChatClient] passing the apikey obtained from your - /// project dashboard. + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. final client = StreamChatClient('b67pax5b2wdq'); WidgetsFlutterBinding.ensureInitialized(); @@ -22,12 +22,13 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: { + extraData: const { 'image': 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', }, ), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.' + 'gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', ); /// Creates a channel using the type `messaging` and `godevs`. @@ -66,12 +67,10 @@ class StreamExample extends StatelessWidget { final Channel channel; @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Stream Chat Dart Example', - home: HomeScreen(channel: channel), - ); - } + Widget build(BuildContext context) => MaterialApp( + title: 'Stream Chat Dart Example', + home: HomeScreen(channel: channel), + ); } /// Main screen of our application. The layout is comprised of an [AppBar] @@ -176,83 +175,81 @@ class _MessageViewState extends State { } @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: _messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = _messages[index]; - if (item.user?.id == widget.channel.client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } - }, + Widget build(BuildContext context) => Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = _messages[index]; + if (item.user?.id == widget.channel.client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } + }, + ), ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - decoration: const InputDecoration( - hintText: 'Enter your message', - ), - ), - ), - Material( - type: MaterialType.circle, - color: Colors.blue, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () async { - // We can send a new message by calling `sendMessage` on - // the current channel. After sending a message, the - // TextField is cleared and the list view is scrolled - // to show the new item. - if (_controller.value.text.isNotEmpty) { - await widget.channel.sendMessage( - Message(text: _controller.value.text), - ); - _controller.clear(); - _updateList(); - } - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Center( - child: Icon( - Icons.send, - color: Colors.white, - ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: const InputDecoration( + hintText: 'Enter your message', ), ), ), - ) - ], - ), - ) - ], - ); - } + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + // We can send a new message by calling `sendMessage` on + // the current channel. After sending a message, the + // TextField is cleared and the list view is scrolled + // to show the new item. + if (_controller.value.text.isNotEmpty) { + await widget.channel.sendMessage( + Message(text: _controller.value.text), + ); + _controller.clear(); + _updateList(); + } + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ), + ) + ], + ), + ) + ], + ); } /// Helper extension for quickly retrieving diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index e6c30c94..8dd6a719 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -8,9 +8,9 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: + cupertino_icons: ^1.0.3 flutter: sdk: flutter - cupertino_icons: ^1.0.0 stream_chat_persistence: path: ../ diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index fe921e70..6a5bc38a 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -416,128 +416,67 @@ class $ChannelsTable extends Channels final String? _alias; $ChannelsTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedTextColumn id = _constructId(); - GeneratedTextColumn _constructId() { - return GeneratedTextColumn( - 'id', - $tableName, - false, - ); - } - + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedTextColumn type = _constructType(); - GeneratedTextColumn _constructType() { - return GeneratedTextColumn( - 'type', - $tableName, - false, - ); - } - + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _cidMeta = const VerificationMeta('cid'); - @override - late final GeneratedTextColumn cid = _constructCid(); - GeneratedTextColumn _constructCid() { - return GeneratedTextColumn( - 'cid', - $tableName, - false, - ); - } - + late final GeneratedColumn cid = GeneratedColumn( + 'cid', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _configMeta = const VerificationMeta('config'); - @override - late final GeneratedTextColumn config = _constructConfig(); - GeneratedTextColumn _constructConfig() { - return GeneratedTextColumn( - 'config', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + config = GeneratedColumn('config', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($ChannelsTable.$converter0); final VerificationMeta _frozenMeta = const VerificationMeta('frozen'); - @override - late final GeneratedBoolColumn frozen = _constructFrozen(); - GeneratedBoolColumn _constructFrozen() { - return GeneratedBoolColumn('frozen', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn frozen = GeneratedColumn( + 'frozen', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (frozen IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _lastMessageAtMeta = const VerificationMeta('lastMessageAt'); - @override - late final GeneratedDateTimeColumn lastMessageAt = _constructLastMessageAt(); - GeneratedDateTimeColumn _constructLastMessageAt() { - return GeneratedDateTimeColumn( - 'last_message_at', - $tableName, - true, - ); - } - + late final GeneratedColumn lastMessageAt = + GeneratedColumn('last_message_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); - @override - late final GeneratedDateTimeColumn updatedAt = _constructUpdatedAt(); - GeneratedDateTimeColumn _constructUpdatedAt() { - return GeneratedDateTimeColumn('updated_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); - @override - late final GeneratedDateTimeColumn deletedAt = _constructDeletedAt(); - GeneratedDateTimeColumn _constructDeletedAt() { - return GeneratedDateTimeColumn( - 'deleted_at', - $tableName, - true, - ); - } - + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _memberCountMeta = const VerificationMeta('memberCount'); - @override - late final GeneratedIntColumn memberCount = _constructMemberCount(); - GeneratedIntColumn _constructMemberCount() { - return GeneratedIntColumn('member_count', $tableName, false, - defaultValue: const Constant(0)); - } - + late final GeneratedColumn memberCount = GeneratedColumn( + 'member_count', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(0)); final VerificationMeta _createdByIdMeta = const VerificationMeta('createdById'); - @override - late final GeneratedTextColumn createdById = _constructCreatedById(); - GeneratedTextColumn _constructCreatedById() { - return GeneratedTextColumn( - 'created_by_id', - $tableName, - true, - ); - } - + late final GeneratedColumn createdById = GeneratedColumn( + 'created_by_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); - @override - late final GeneratedTextColumn extraData = _constructExtraData(); - GeneratedTextColumn _constructExtraData() { - return GeneratedTextColumn( - 'extra_data', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($ChannelsTable.$converter1); @override List get $columns => [ id, @@ -554,11 +493,9 @@ class $ChannelsTable extends Channels extraData ]; @override - $ChannelsTable get asDslTable => this; + String get aliasedName => _alias ?? 'channels'; @override - String get $tableName => _alias ?? 'channels'; - @override - final String actualTableName = 'channels'; + String get actualTableName => 'channels'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -624,8 +561,8 @@ class $ChannelsTable extends Channels Set get $primaryKey => {cid}; @override ChannelEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return ChannelEntity.fromData(data, _db, prefix: effectivePrefix); + return ChannelEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -1405,257 +1342,137 @@ class $MessagesTable extends Messages final String? _alias; $MessagesTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedTextColumn id = _constructId(); - GeneratedTextColumn _constructId() { - return GeneratedTextColumn( - 'id', - $tableName, - false, - ); - } - + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _messageTextMeta = const VerificationMeta('messageText'); - @override - late final GeneratedTextColumn messageText = _constructMessageText(); - GeneratedTextColumn _constructMessageText() { - return GeneratedTextColumn( - 'message_text', - $tableName, - true, - ); - } - + late final GeneratedColumn messageText = GeneratedColumn( + 'message_text', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _attachmentsMeta = const VerificationMeta('attachments'); - @override - late final GeneratedTextColumn attachments = _constructAttachments(); - GeneratedTextColumn _constructAttachments() { - return GeneratedTextColumn( - 'attachments', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + attachments = GeneratedColumn('attachments', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($MessagesTable.$converter0); final VerificationMeta _statusMeta = const VerificationMeta('status'); - @override - late final GeneratedIntColumn status = _constructStatus(); - GeneratedIntColumn _constructStatus() { - return GeneratedIntColumn('status', $tableName, false, - defaultValue: const Constant(1)); - } - + late final GeneratedColumnWithTypeConverter + status = GeneratedColumn('status', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(1)) + .withConverter($MessagesTable.$converter1); final VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedTextColumn type = _constructType(); - GeneratedTextColumn _constructType() { - return GeneratedTextColumn('type', $tableName, false, - defaultValue: const Constant('regular')); - } - + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: false, + defaultValue: const Constant('regular')); final VerificationMeta _mentionedUsersMeta = const VerificationMeta('mentionedUsers'); - @override - late final GeneratedTextColumn mentionedUsers = _constructMentionedUsers(); - GeneratedTextColumn _constructMentionedUsers() { - return GeneratedTextColumn( - 'mentioned_users', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + mentionedUsers = GeneratedColumn( + 'mentioned_users', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($MessagesTable.$converter2); final VerificationMeta _reactionCountsMeta = const VerificationMeta('reactionCounts'); - @override - late final GeneratedTextColumn reactionCounts = _constructReactionCounts(); - GeneratedTextColumn _constructReactionCounts() { - return GeneratedTextColumn( - 'reaction_counts', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + reactionCounts = GeneratedColumn( + 'reaction_counts', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($MessagesTable.$converter3); final VerificationMeta _reactionScoresMeta = const VerificationMeta('reactionScores'); - @override - late final GeneratedTextColumn reactionScores = _constructReactionScores(); - GeneratedTextColumn _constructReactionScores() { - return GeneratedTextColumn( - 'reaction_scores', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + reactionScores = GeneratedColumn( + 'reaction_scores', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($MessagesTable.$converter4); final VerificationMeta _parentIdMeta = const VerificationMeta('parentId'); - @override - late final GeneratedTextColumn parentId = _constructParentId(); - GeneratedTextColumn _constructParentId() { - return GeneratedTextColumn( - 'parent_id', - $tableName, - true, - ); - } - + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _quotedMessageIdMeta = const VerificationMeta('quotedMessageId'); - @override - late final GeneratedTextColumn quotedMessageId = _constructQuotedMessageId(); - GeneratedTextColumn _constructQuotedMessageId() { - return GeneratedTextColumn( - 'quoted_message_id', - $tableName, - true, - ); - } - + late final GeneratedColumn quotedMessageId = + GeneratedColumn('quoted_message_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _replyCountMeta = const VerificationMeta('replyCount'); - @override - late final GeneratedIntColumn replyCount = _constructReplyCount(); - GeneratedIntColumn _constructReplyCount() { - return GeneratedIntColumn( - 'reply_count', - $tableName, - true, - ); - } - + late final GeneratedColumn replyCount = GeneratedColumn( + 'reply_count', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _showInChannelMeta = const VerificationMeta('showInChannel'); - @override - late final GeneratedBoolColumn showInChannel = _constructShowInChannel(); - GeneratedBoolColumn _constructShowInChannel() { - return GeneratedBoolColumn( - 'show_in_channel', - $tableName, - true, - ); - } - + late final GeneratedColumn showInChannel = GeneratedColumn( + 'show_in_channel', aliasedName, true, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (show_in_channel IN (0, 1))'); final VerificationMeta _shadowedMeta = const VerificationMeta('shadowed'); - @override - late final GeneratedBoolColumn shadowed = _constructShadowed(); - GeneratedBoolColumn _constructShadowed() { - return GeneratedBoolColumn('shadowed', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn shadowed = GeneratedColumn( + 'shadowed', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (shadowed IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _commandMeta = const VerificationMeta('command'); - @override - late final GeneratedTextColumn command = _constructCommand(); - GeneratedTextColumn _constructCommand() { - return GeneratedTextColumn( - 'command', - $tableName, - true, - ); - } - + late final GeneratedColumn command = GeneratedColumn( + 'command', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); - @override - late final GeneratedDateTimeColumn updatedAt = _constructUpdatedAt(); - GeneratedDateTimeColumn _constructUpdatedAt() { - return GeneratedDateTimeColumn('updated_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); - @override - late final GeneratedDateTimeColumn deletedAt = _constructDeletedAt(); - GeneratedDateTimeColumn _constructDeletedAt() { - return GeneratedDateTimeColumn( - 'deleted_at', - $tableName, - true, - ); - } - + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _userIdMeta = const VerificationMeta('userId'); - @override - late final GeneratedTextColumn userId = _constructUserId(); - GeneratedTextColumn _constructUserId() { - return GeneratedTextColumn( - 'user_id', - $tableName, - true, - ); - } - + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); - @override - late final GeneratedBoolColumn pinned = _constructPinned(); - GeneratedBoolColumn _constructPinned() { - return GeneratedBoolColumn('pinned', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (pinned IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _pinnedAtMeta = const VerificationMeta('pinnedAt'); - @override - late final GeneratedDateTimeColumn pinnedAt = _constructPinnedAt(); - GeneratedDateTimeColumn _constructPinnedAt() { - return GeneratedDateTimeColumn( - 'pinned_at', - $tableName, - true, - ); - } - + late final GeneratedColumn pinnedAt = GeneratedColumn( + 'pinned_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _pinExpiresMeta = const VerificationMeta('pinExpires'); - @override - late final GeneratedDateTimeColumn pinExpires = _constructPinExpires(); - GeneratedDateTimeColumn _constructPinExpires() { - return GeneratedDateTimeColumn( - 'pin_expires', - $tableName, - true, - ); - } - + late final GeneratedColumn pinExpires = GeneratedColumn( + 'pin_expires', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _pinnedByUserIdMeta = const VerificationMeta('pinnedByUserId'); - @override - late final GeneratedTextColumn pinnedByUserId = _constructPinnedByUserId(); - GeneratedTextColumn _constructPinnedByUserId() { - return GeneratedTextColumn( - 'pinned_by_user_id', - $tableName, - true, - ); - } - + late final GeneratedColumn pinnedByUserId = GeneratedColumn( + 'pinned_by_user_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); - @override - late final GeneratedTextColumn channelCid = _constructChannelCid(); - GeneratedTextColumn _constructChannelCid() { - return GeneratedTextColumn('channel_cid', $tableName, true, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); - } - + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, true, + typeName: 'TEXT', + requiredDuringInsert: false, + $customConstraints: + 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); - @override - late final GeneratedTextColumn extraData = _constructExtraData(); - GeneratedTextColumn _constructExtraData() { - return GeneratedTextColumn( - 'extra_data', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($MessagesTable.$converter5); @override List get $columns => [ id, @@ -1684,11 +1501,9 @@ class $MessagesTable extends Messages extraData ]; @override - $MessagesTable get asDslTable => this; + String get aliasedName => _alias ?? 'messages'; @override - String get $tableName => _alias ?? 'messages'; - @override - final String actualTableName = 'messages'; + String get actualTableName => 'messages'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -1794,8 +1609,8 @@ class $MessagesTable extends Messages Set get $primaryKey => {id}; @override MessageEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return MessageEntity.fromData(data, _db, prefix: effectivePrefix); + return MessageEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -2587,257 +2402,139 @@ class $PinnedMessagesTable extends PinnedMessages final String? _alias; $PinnedMessagesTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedTextColumn id = _constructId(); - GeneratedTextColumn _constructId() { - return GeneratedTextColumn( - 'id', - $tableName, - false, - ); - } - + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _messageTextMeta = const VerificationMeta('messageText'); - @override - late final GeneratedTextColumn messageText = _constructMessageText(); - GeneratedTextColumn _constructMessageText() { - return GeneratedTextColumn( - 'message_text', - $tableName, - true, - ); - } - + late final GeneratedColumn messageText = GeneratedColumn( + 'message_text', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _attachmentsMeta = const VerificationMeta('attachments'); - @override - late final GeneratedTextColumn attachments = _constructAttachments(); - GeneratedTextColumn _constructAttachments() { - return GeneratedTextColumn( - 'attachments', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + attachments = GeneratedColumn('attachments', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($PinnedMessagesTable.$converter0); final VerificationMeta _statusMeta = const VerificationMeta('status'); - @override - late final GeneratedIntColumn status = _constructStatus(); - GeneratedIntColumn _constructStatus() { - return GeneratedIntColumn('status', $tableName, false, - defaultValue: const Constant(1)); - } - + late final GeneratedColumnWithTypeConverter + status = GeneratedColumn('status', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(1)) + .withConverter( + $PinnedMessagesTable.$converter1); final VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedTextColumn type = _constructType(); - GeneratedTextColumn _constructType() { - return GeneratedTextColumn('type', $tableName, false, - defaultValue: const Constant('regular')); - } - + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: false, + defaultValue: const Constant('regular')); final VerificationMeta _mentionedUsersMeta = const VerificationMeta('mentionedUsers'); - @override - late final GeneratedTextColumn mentionedUsers = _constructMentionedUsers(); - GeneratedTextColumn _constructMentionedUsers() { - return GeneratedTextColumn( - 'mentioned_users', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + mentionedUsers = GeneratedColumn( + 'mentioned_users', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($PinnedMessagesTable.$converter2); final VerificationMeta _reactionCountsMeta = const VerificationMeta('reactionCounts'); - @override - late final GeneratedTextColumn reactionCounts = _constructReactionCounts(); - GeneratedTextColumn _constructReactionCounts() { - return GeneratedTextColumn( - 'reaction_counts', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + reactionCounts = GeneratedColumn( + 'reaction_counts', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($PinnedMessagesTable.$converter3); final VerificationMeta _reactionScoresMeta = const VerificationMeta('reactionScores'); - @override - late final GeneratedTextColumn reactionScores = _constructReactionScores(); - GeneratedTextColumn _constructReactionScores() { - return GeneratedTextColumn( - 'reaction_scores', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + reactionScores = GeneratedColumn( + 'reaction_scores', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($PinnedMessagesTable.$converter4); final VerificationMeta _parentIdMeta = const VerificationMeta('parentId'); - @override - late final GeneratedTextColumn parentId = _constructParentId(); - GeneratedTextColumn _constructParentId() { - return GeneratedTextColumn( - 'parent_id', - $tableName, - true, - ); - } - + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _quotedMessageIdMeta = const VerificationMeta('quotedMessageId'); - @override - late final GeneratedTextColumn quotedMessageId = _constructQuotedMessageId(); - GeneratedTextColumn _constructQuotedMessageId() { - return GeneratedTextColumn( - 'quoted_message_id', - $tableName, - true, - ); - } - + late final GeneratedColumn quotedMessageId = + GeneratedColumn('quoted_message_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _replyCountMeta = const VerificationMeta('replyCount'); - @override - late final GeneratedIntColumn replyCount = _constructReplyCount(); - GeneratedIntColumn _constructReplyCount() { - return GeneratedIntColumn( - 'reply_count', - $tableName, - true, - ); - } - + late final GeneratedColumn replyCount = GeneratedColumn( + 'reply_count', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _showInChannelMeta = const VerificationMeta('showInChannel'); - @override - late final GeneratedBoolColumn showInChannel = _constructShowInChannel(); - GeneratedBoolColumn _constructShowInChannel() { - return GeneratedBoolColumn( - 'show_in_channel', - $tableName, - true, - ); - } - + late final GeneratedColumn showInChannel = GeneratedColumn( + 'show_in_channel', aliasedName, true, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (show_in_channel IN (0, 1))'); final VerificationMeta _shadowedMeta = const VerificationMeta('shadowed'); - @override - late final GeneratedBoolColumn shadowed = _constructShadowed(); - GeneratedBoolColumn _constructShadowed() { - return GeneratedBoolColumn('shadowed', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn shadowed = GeneratedColumn( + 'shadowed', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (shadowed IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _commandMeta = const VerificationMeta('command'); - @override - late final GeneratedTextColumn command = _constructCommand(); - GeneratedTextColumn _constructCommand() { - return GeneratedTextColumn( - 'command', - $tableName, - true, - ); - } - + late final GeneratedColumn command = GeneratedColumn( + 'command', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); - @override - late final GeneratedDateTimeColumn updatedAt = _constructUpdatedAt(); - GeneratedDateTimeColumn _constructUpdatedAt() { - return GeneratedDateTimeColumn('updated_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); - @override - late final GeneratedDateTimeColumn deletedAt = _constructDeletedAt(); - GeneratedDateTimeColumn _constructDeletedAt() { - return GeneratedDateTimeColumn( - 'deleted_at', - $tableName, - true, - ); - } - + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _userIdMeta = const VerificationMeta('userId'); - @override - late final GeneratedTextColumn userId = _constructUserId(); - GeneratedTextColumn _constructUserId() { - return GeneratedTextColumn( - 'user_id', - $tableName, - true, - ); - } - + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); - @override - late final GeneratedBoolColumn pinned = _constructPinned(); - GeneratedBoolColumn _constructPinned() { - return GeneratedBoolColumn('pinned', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (pinned IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _pinnedAtMeta = const VerificationMeta('pinnedAt'); - @override - late final GeneratedDateTimeColumn pinnedAt = _constructPinnedAt(); - GeneratedDateTimeColumn _constructPinnedAt() { - return GeneratedDateTimeColumn( - 'pinned_at', - $tableName, - true, - ); - } - + late final GeneratedColumn pinnedAt = GeneratedColumn( + 'pinned_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _pinExpiresMeta = const VerificationMeta('pinExpires'); - @override - late final GeneratedDateTimeColumn pinExpires = _constructPinExpires(); - GeneratedDateTimeColumn _constructPinExpires() { - return GeneratedDateTimeColumn( - 'pin_expires', - $tableName, - true, - ); - } - + late final GeneratedColumn pinExpires = GeneratedColumn( + 'pin_expires', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _pinnedByUserIdMeta = const VerificationMeta('pinnedByUserId'); - @override - late final GeneratedTextColumn pinnedByUserId = _constructPinnedByUserId(); - GeneratedTextColumn _constructPinnedByUserId() { - return GeneratedTextColumn( - 'pinned_by_user_id', - $tableName, - true, - ); - } - + late final GeneratedColumn pinnedByUserId = GeneratedColumn( + 'pinned_by_user_id', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); - @override - late final GeneratedTextColumn channelCid = _constructChannelCid(); - GeneratedTextColumn _constructChannelCid() { - return GeneratedTextColumn('channel_cid', $tableName, true, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); - } - + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, true, + typeName: 'TEXT', + requiredDuringInsert: false, + $customConstraints: + 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); - @override - late final GeneratedTextColumn extraData = _constructExtraData(); - GeneratedTextColumn _constructExtraData() { - return GeneratedTextColumn( - 'extra_data', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>( + $PinnedMessagesTable.$converter5); @override List get $columns => [ id, @@ -2866,11 +2563,9 @@ class $PinnedMessagesTable extends PinnedMessages extraData ]; @override - $PinnedMessagesTable get asDslTable => this; + String get aliasedName => _alias ?? 'pinned_messages'; @override - String get $tableName => _alias ?? 'pinned_messages'; - @override - final String actualTableName = 'pinned_messages'; + String get actualTableName => 'pinned_messages'; @override VerificationContext validateIntegrity( Insertable instance, @@ -2977,8 +2672,8 @@ class $PinnedMessagesTable extends PinnedMessages Set get $primaryKey => {id}; @override PinnedMessageEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return PinnedMessageEntity.fromData(data, _db, prefix: effectivePrefix); + return PinnedMessageEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -3239,71 +2934,43 @@ class $ReactionsTable extends Reactions final String? _alias; $ReactionsTable(this._db, [this._alias]); final VerificationMeta _userIdMeta = const VerificationMeta('userId'); - @override - late final GeneratedTextColumn userId = _constructUserId(); - GeneratedTextColumn _constructUserId() { - return GeneratedTextColumn( - 'user_id', - $tableName, - false, - ); - } - + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); - @override - late final GeneratedTextColumn messageId = _constructMessageId(); - GeneratedTextColumn _constructMessageId() { - return GeneratedTextColumn('message_id', $tableName, false, - $customConstraints: 'REFERENCES messages(id) ON DELETE CASCADE'); - } - + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: true, + $customConstraints: 'REFERENCES messages(id) ON DELETE CASCADE'); final VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedTextColumn type = _constructType(); - GeneratedTextColumn _constructType() { - return GeneratedTextColumn( - 'type', - $tableName, - false, - ); - } - + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _scoreMeta = const VerificationMeta('score'); - @override - late final GeneratedIntColumn score = _constructScore(); - GeneratedIntColumn _constructScore() { - return GeneratedIntColumn('score', $tableName, false, - defaultValue: const Constant(0)); - } - + late final GeneratedColumn score = GeneratedColumn( + 'score', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(0)); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); - @override - late final GeneratedTextColumn extraData = _constructExtraData(); - GeneratedTextColumn _constructExtraData() { - return GeneratedTextColumn( - 'extra_data', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($ReactionsTable.$converter0); @override List get $columns => [userId, messageId, type, createdAt, score, extraData]; @override - $ReactionsTable get asDslTable => this; + String get aliasedName => _alias ?? 'reactions'; @override - String get $tableName => _alias ?? 'reactions'; - @override - final String actualTableName = 'reactions'; + String get actualTableName => 'reactions'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -3343,8 +3010,8 @@ class $ReactionsTable extends Reactions Set get $primaryKey => {messageId, type, userId}; @override ReactionEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return ReactionEntity.fromData(data, _db, prefix: effectivePrefix); + return ReactionEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -3648,90 +3315,55 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { final String? _alias; $UsersTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedTextColumn id = _constructId(); - GeneratedTextColumn _constructId() { - return GeneratedTextColumn( - 'id', - $tableName, - false, - ); - } - + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _roleMeta = const VerificationMeta('role'); - @override - late final GeneratedTextColumn role = _constructRole(); - GeneratedTextColumn _constructRole() { - return GeneratedTextColumn( - 'role', - $tableName, - true, - ); - } - + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); - @override - late final GeneratedDateTimeColumn updatedAt = _constructUpdatedAt(); - GeneratedDateTimeColumn _constructUpdatedAt() { - return GeneratedDateTimeColumn('updated_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _lastActiveMeta = const VerificationMeta('lastActive'); - @override - late final GeneratedDateTimeColumn lastActive = _constructLastActive(); - GeneratedDateTimeColumn _constructLastActive() { - return GeneratedDateTimeColumn( - 'last_active', - $tableName, - true, - ); - } - + late final GeneratedColumn lastActive = GeneratedColumn( + 'last_active', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _onlineMeta = const VerificationMeta('online'); - @override - late final GeneratedBoolColumn online = _constructOnline(); - GeneratedBoolColumn _constructOnline() { - return GeneratedBoolColumn('online', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn online = GeneratedColumn( + 'online', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (online IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _bannedMeta = const VerificationMeta('banned'); - @override - late final GeneratedBoolColumn banned = _constructBanned(); - GeneratedBoolColumn _constructBanned() { - return GeneratedBoolColumn('banned', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn banned = GeneratedColumn( + 'banned', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (banned IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); - @override - late final GeneratedTextColumn extraData = _constructExtraData(); - GeneratedTextColumn _constructExtraData() { - return GeneratedTextColumn( - 'extra_data', - $tableName, - false, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true) + .withConverter>($UsersTable.$converter0); @override List get $columns => [id, role, createdAt, updatedAt, lastActive, online, banned, extraData]; @override - $UsersTable get asDslTable => this; + String get aliasedName => _alias ?? 'users'; @override - String get $tableName => _alias ?? 'users'; - @override - final String actualTableName = 'users'; + String get actualTableName => 'users'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -3776,8 +3408,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { Set get $primaryKey => {id}; @override UserEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return UserEntity.fromData(data, _db, prefix: effectivePrefix); + return UserEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -4165,111 +3797,71 @@ class $MembersTable extends Members final String? _alias; $MembersTable(this._db, [this._alias]); final VerificationMeta _userIdMeta = const VerificationMeta('userId'); - @override - late final GeneratedTextColumn userId = _constructUserId(); - GeneratedTextColumn _constructUserId() { - return GeneratedTextColumn( - 'user_id', - $tableName, - false, - ); - } - + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); - @override - late final GeneratedTextColumn channelCid = _constructChannelCid(); - GeneratedTextColumn _constructChannelCid() { - return GeneratedTextColumn('channel_cid', $tableName, false, - $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); - } - + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _roleMeta = const VerificationMeta('role'); - @override - late final GeneratedTextColumn role = _constructRole(); - GeneratedTextColumn _constructRole() { - return GeneratedTextColumn( - 'role', - $tableName, - true, - ); - } - + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _inviteAcceptedAtMeta = const VerificationMeta('inviteAcceptedAt'); - @override - late final GeneratedDateTimeColumn inviteAcceptedAt = - _constructInviteAcceptedAt(); - GeneratedDateTimeColumn _constructInviteAcceptedAt() { - return GeneratedDateTimeColumn( - 'invite_accepted_at', - $tableName, - true, - ); - } - + late final GeneratedColumn inviteAcceptedAt = + GeneratedColumn('invite_accepted_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _inviteRejectedAtMeta = const VerificationMeta('inviteRejectedAt'); - @override - late final GeneratedDateTimeColumn inviteRejectedAt = - _constructInviteRejectedAt(); - GeneratedDateTimeColumn _constructInviteRejectedAt() { - return GeneratedDateTimeColumn( - 'invite_rejected_at', - $tableName, - true, - ); - } - + late final GeneratedColumn inviteRejectedAt = + GeneratedColumn('invite_rejected_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _invitedMeta = const VerificationMeta('invited'); - @override - late final GeneratedBoolColumn invited = _constructInvited(); - GeneratedBoolColumn _constructInvited() { - return GeneratedBoolColumn('invited', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn invited = GeneratedColumn( + 'invited', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (invited IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _bannedMeta = const VerificationMeta('banned'); - @override - late final GeneratedBoolColumn banned = _constructBanned(); - GeneratedBoolColumn _constructBanned() { - return GeneratedBoolColumn('banned', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn banned = GeneratedColumn( + 'banned', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (banned IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _shadowBannedMeta = const VerificationMeta('shadowBanned'); - @override - late final GeneratedBoolColumn shadowBanned = _constructShadowBanned(); - GeneratedBoolColumn _constructShadowBanned() { - return GeneratedBoolColumn('shadow_banned', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn shadowBanned = GeneratedColumn( + 'shadow_banned', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (shadow_banned IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _isModeratorMeta = const VerificationMeta('isModerator'); - @override - late final GeneratedBoolColumn isModerator = _constructIsModerator(); - GeneratedBoolColumn _constructIsModerator() { - return GeneratedBoolColumn('is_moderator', $tableName, false, - defaultValue: const Constant(false)); - } - + late final GeneratedColumn isModerator = GeneratedColumn( + 'is_moderator', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultConstraints: 'CHECK (is_moderator IN (0, 1))', + defaultValue: const Constant(false)); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); - @override - late final GeneratedDateTimeColumn createdAt = _constructCreatedAt(); - GeneratedDateTimeColumn _constructCreatedAt() { - return GeneratedDateTimeColumn('created_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); - @override - late final GeneratedDateTimeColumn updatedAt = _constructUpdatedAt(); - GeneratedDateTimeColumn _constructUpdatedAt() { - return GeneratedDateTimeColumn('updated_at', $tableName, false, - defaultValue: currentDateAndTime); - } - + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); @override List get $columns => [ userId, @@ -4285,11 +3877,9 @@ class $MembersTable extends Members updatedAt ]; @override - $MembersTable get asDslTable => this; + String get aliasedName => _alias ?? 'members'; @override - String get $tableName => _alias ?? 'members'; - @override - final String actualTableName = 'members'; + String get actualTableName => 'members'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -4360,8 +3950,8 @@ class $MembersTable extends Members Set get $primaryKey => {userId, channelCid}; @override MemberEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return MemberEntity.fromData(data, _db, prefix: effectivePrefix); + return MemberEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -4550,53 +4140,33 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { final String? _alias; $ReadsTable(this._db, [this._alias]); final VerificationMeta _lastReadMeta = const VerificationMeta('lastRead'); - @override - late final GeneratedDateTimeColumn lastRead = _constructLastRead(); - GeneratedDateTimeColumn _constructLastRead() { - return GeneratedDateTimeColumn( - 'last_read', - $tableName, - false, - ); - } - + late final GeneratedColumn lastRead = GeneratedColumn( + 'last_read', aliasedName, false, + typeName: 'INTEGER', requiredDuringInsert: true); final VerificationMeta _userIdMeta = const VerificationMeta('userId'); - @override - late final GeneratedTextColumn userId = _constructUserId(); - GeneratedTextColumn _constructUserId() { - return GeneratedTextColumn( - 'user_id', - $tableName, - false, - ); - } - + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); - @override - late final GeneratedTextColumn channelCid = _constructChannelCid(); - GeneratedTextColumn _constructChannelCid() { - return GeneratedTextColumn('channel_cid', $tableName, false, - $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); - } - + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + typeName: 'TEXT', + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _unreadMessagesMeta = const VerificationMeta('unreadMessages'); - @override - late final GeneratedIntColumn unreadMessages = _constructUnreadMessages(); - GeneratedIntColumn _constructUnreadMessages() { - return GeneratedIntColumn('unread_messages', $tableName, false, - defaultValue: const Constant(0)); - } - + late final GeneratedColumn unreadMessages = GeneratedColumn( + 'unread_messages', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(0)); @override List get $columns => [lastRead, userId, channelCid, unreadMessages]; @override - $ReadsTable get asDslTable => this; + String get aliasedName => _alias ?? 'reads'; @override - String get $tableName => _alias ?? 'reads'; - @override - final String actualTableName = 'reads'; + String get actualTableName => 'reads'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -4635,8 +4205,8 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { Set get $primaryKey => {userId, channelCid}; @override ReadEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return ReadEntity.fromData(data, _db, prefix: effectivePrefix); + return ReadEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -4771,35 +4341,19 @@ class $ChannelQueriesTable extends ChannelQueries final String? _alias; $ChannelQueriesTable(this._db, [this._alias]); final VerificationMeta _queryHashMeta = const VerificationMeta('queryHash'); - @override - late final GeneratedTextColumn queryHash = _constructQueryHash(); - GeneratedTextColumn _constructQueryHash() { - return GeneratedTextColumn( - 'query_hash', - $tableName, - false, - ); - } - + late final GeneratedColumn queryHash = GeneratedColumn( + 'query_hash', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); - @override - late final GeneratedTextColumn channelCid = _constructChannelCid(); - GeneratedTextColumn _constructChannelCid() { - return GeneratedTextColumn( - 'channel_cid', - $tableName, - false, - ); - } - + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); @override List get $columns => [queryHash, channelCid]; @override - $ChannelQueriesTable get asDslTable => this; + String get aliasedName => _alias ?? 'channel_queries'; @override - String get $tableName => _alias ?? 'channel_queries'; - @override - final String actualTableName = 'channel_queries'; + String get actualTableName => 'channel_queries'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -4826,8 +4380,8 @@ class $ChannelQueriesTable extends ChannelQueries Set get $primaryKey => {queryHash, channelCid}; @override ChannelQueryEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return ChannelQueryEntity.fromData(data, _db, prefix: effectivePrefix); + return ChannelQueryEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override @@ -5110,85 +4664,38 @@ class $ConnectionEventsTable extends ConnectionEvents final String? _alias; $ConnectionEventsTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedIntColumn id = _constructId(); - GeneratedIntColumn _constructId() { - return GeneratedIntColumn( - 'id', - $tableName, - false, - ); - } - + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedTextColumn type = _constructType(); - GeneratedTextColumn _constructType() { - return GeneratedTextColumn( - 'type', - $tableName, - false, - ); - } - + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); final VerificationMeta _ownUserMeta = const VerificationMeta('ownUser'); - @override - late final GeneratedTextColumn ownUser = _constructOwnUser(); - GeneratedTextColumn _constructOwnUser() { - return GeneratedTextColumn( - 'own_user', - $tableName, - true, - ); - } - + late final GeneratedColumnWithTypeConverter, String?> + ownUser = GeneratedColumn('own_user', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>( + $ConnectionEventsTable.$converter0); final VerificationMeta _totalUnreadCountMeta = const VerificationMeta('totalUnreadCount'); - @override - late final GeneratedIntColumn totalUnreadCount = _constructTotalUnreadCount(); - GeneratedIntColumn _constructTotalUnreadCount() { - return GeneratedIntColumn( - 'total_unread_count', - $tableName, - true, - ); - } - + late final GeneratedColumn totalUnreadCount = GeneratedColumn( + 'total_unread_count', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _unreadChannelsMeta = const VerificationMeta('unreadChannels'); - @override - late final GeneratedIntColumn unreadChannels = _constructUnreadChannels(); - GeneratedIntColumn _constructUnreadChannels() { - return GeneratedIntColumn( - 'unread_channels', - $tableName, - true, - ); - } - + late final GeneratedColumn unreadChannels = GeneratedColumn( + 'unread_channels', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _lastEventAtMeta = const VerificationMeta('lastEventAt'); - @override - late final GeneratedDateTimeColumn lastEventAt = _constructLastEventAt(); - GeneratedDateTimeColumn _constructLastEventAt() { - return GeneratedDateTimeColumn( - 'last_event_at', - $tableName, - true, - ); - } - + late final GeneratedColumn lastEventAt = + GeneratedColumn('last_event_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); final VerificationMeta _lastSyncAtMeta = const VerificationMeta('lastSyncAt'); - @override - late final GeneratedDateTimeColumn lastSyncAt = _constructLastSyncAt(); - GeneratedDateTimeColumn _constructLastSyncAt() { - return GeneratedDateTimeColumn( - 'last_sync_at', - $tableName, - true, - ); - } - + late final GeneratedColumn lastSyncAt = GeneratedColumn( + 'last_sync_at', aliasedName, true, + typeName: 'INTEGER', requiredDuringInsert: false); @override List get $columns => [ id, @@ -5200,11 +4707,9 @@ class $ConnectionEventsTable extends ConnectionEvents lastSyncAt ]; @override - $ConnectionEventsTable get asDslTable => this; + String get aliasedName => _alias ?? 'connection_events'; @override - String get $tableName => _alias ?? 'connection_events'; - @override - final String actualTableName = 'connection_events'; + String get actualTableName => 'connection_events'; @override VerificationContext validateIntegrity( Insertable instance, @@ -5252,8 +4757,8 @@ class $ConnectionEventsTable extends ConnectionEvents Set get $primaryKey => {id}; @override ConnectionEventEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return ConnectionEventEntity.fromData(data, _db, prefix: effectivePrefix); + return ConnectionEventEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); } @override diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index e5fb8fe6..08029e91 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 2.0.0-nullsafety.7 +version: 2.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -13,12 +13,12 @@ dependencies: sdk: flutter logging: ^1.0.1 meta: ^1.3.0 - moor: ^4.2.1 + moor: ^4.4.0 mutex: ^3.0.0 path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^2.0.0-nullsafety.7 + stream_chat: ^2.0.0 dev_dependencies: build_runner: ^2.0.1 diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index a69a07ee..62a3df15 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -213,8 +213,8 @@ void main() { test('should return sorted channels using custom field', () async { int sortComparator(ChannelModel a, ChannelModel b) { - final aData = a.extraData['test_custom_field'] as int; - final bData = b.extraData['test_custom_field'] as int; + final aData = int.parse(a.extraData['test_custom_field'].toString()); + final bData = int.parse(b.extraData['test_custom_field'].toString()); return bData.compareTo(aData); } diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 2c852ff2..7d867deb 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -173,8 +173,8 @@ void main() { expect(entity.shadowed, message.shadowed); expect(entity.showInChannel, message.showInChannel); expect(entity.replyCount, message.replyCount); - expect(entity.mentionedUsers, - message.mentionedUsers.map((e) => jsonEncode(e)).toList()); + expect( + entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); expect(entity.reactionScores, message.reactionScores); expect(entity.reactionCounts, message.reactionCounts); expect(entity.status, message.status); diff --git a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart index adc05c58..4454ad73 100644 --- a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart @@ -39,7 +39,7 @@ void main() { lastActive: DateTime.now(), online: math.Random().nextBool(), banned: math.Random().nextBool(), - extraData: {'test_extra_data': 'extraData'}, + extraData: const {'test_extra_data': 'extraData'}, ); final entity = user.toEntity(); expect(entity, isA());