diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fbe1ab13..508508a7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,26 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - - package-ecosystem: "pub" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pub" + directory: "/packages/stream_chat" + schedule: + interval: "weekly" + - package-ecosystem: "pub" + directory: "/packages/stream_chat_flutter_core" + schedule: + interval: "weekly" + - package-ecosystem: "pub" + directory: "/packages/stream_chat_flutter" + schedule: + interval: "weekly" + - package-ecosystem: "pub" + directory: "/packages/stream_chat_persistence" + schedule: + interval: "weekly" + - package-ecosystem: "pub" + directory: "/packages/stream_chat_localizations" schedule: interval: "weekly" diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml deleted file mode 100644 index 504173f3..00000000 --- a/.github/workflows/dart_code_metrics.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Dart Code Metrics - -env: - flutter_version: "3.10.4" - folders: "lib, test" - -on: - pull_request: - paths: - - 'packages/**' - push: - branches: - - master - - develop - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - check: - name: dart-code-metrics - runs-on: ubuntu-latest - steps: - - name: "Git Checkout" - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - cache: true - flutter-version: ${{ env.flutter_version }} - - - name: "Install Tools" - run: flutter pub global activate melos - - name: "Bootstrap Workspace" - run: melos bootstrap - - - name: "Stream Chat Metrics" - uses: dart-code-checker/dart-code-metrics-action@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - relative_path: 'packages/stream_chat' - folders: ${{ env.folders }} - - - name: "Stream Chat Flutter Core Metrics" - uses: dart-code-checker/dart-code-metrics-action@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - relative_path: 'packages/stream_chat_flutter_core' - folders: ${{ env.folders }} - - - name: "Stream Chat Flutter Metrics" - uses: dart-code-checker/dart-code-metrics-action@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - relative_path: 'packages/stream_chat_flutter' - folders: ${{ env.folders }} - - - name: "Stream Chat Localizations Metrics" - uses: dart-code-checker/dart-code-metrics-action@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - relative_path: 'packages/stream_chat_localizations' - folders: ${{ env.folders }} - - - name: "Stream Chat Persistence Metrics" - uses: dart-code-checker/dart-code-metrics-action@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - relative_path: 'packages/stream_chat_persistence' - folders: ${{ env.folders }} diff --git a/.github/workflows/dispatch_workflows.yml b/.github/workflows/dispatch_workflows.yml index 6d626fdb..afd04f99 100644 --- a/.github/workflows/dispatch_workflows.yml +++ b/.github/workflows/dispatch_workflows.yml @@ -14,7 +14,7 @@ jobs: if: github.ref == 'refs/heads/develop' runs-on: ubuntu-latest steps: - - uses: benc-uk/workflow-dispatch@v1 + - uses: benc-uk/workflow-dispatch@v121 with: workflow: build_nightly repo: GetStream/flutter-samples @@ -23,7 +23,7 @@ jobs: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - - uses: benc-uk/workflow-dispatch@v1 + - uses: benc-uk/workflow-dispatch@v121 with: repo: GetStream/flutter-samples workflow: build diff --git a/.github/workflows/legacy_version_analyze.yml b/.github/workflows/legacy_version_analyze.yml new file mode 100644 index 00000000..89c195df --- /dev/null +++ b/.github/workflows/legacy_version_analyze.yml @@ -0,0 +1,71 @@ +name: legacy_version_analyze + +env: + # Note: The versions below should be manually updated after a new stable + # version comes out. + flutter_version: "3.7.12" + +on: + push: + branches: + - master + - develop + paths: + - 'packages/**' + - '.github/workflows/legacy_version_analyze.yml' + pull_request: + branches: + - master + - develop + paths: + - 'packages/**' + - '.github/workflows/legacy_version_analyze.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Does a sanity check that packages at least pass analysis on the N-1 + # versions of Flutter stable if the package claims to support that version. + # This is to minimize accidentally making changes that break old versions + # (which we don't commit to supporting, but don't want to actively break) + # without updating the constraints. + analyze_legacy_versions: + timeout-minutes: 15 + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: "Git Checkout" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: "Install Flutter" + uses: subosito/flutter-action@v2 + with: + cache: true + flutter-version: ${{ env.flutter_version }} + + - name: "Install Tools" + run: flutter pub global activate melos + + - name: "Bootstrap Workspace" + run: melos bootstrap --verbose + + # Only analyze lib/; non-client code doesn't need to work on + # all supported legacy version. + - name: "Stream Chat Analyze" + run: cd packages/stream_chat/lib && dart analyze --fatal-infos . + + - name: "Stream Chat Flutter Core Analyze" + run: cd packages/stream_chat_flutter_core/lib && dart analyze --fatal-infos . + + - name: "Stream Chat Flutter Analyze" + run: cd packages/stream_chat_flutter/lib && dart analyze --fatal-infos . + + - name: "Stream Chat Persistence Analyze" + run: cd packages/stream_chat_persistence/lib && dart analyze --fatal-infos . + + - name: "Stream Chat Localizations Analyze" + run: cd packages/stream_chat_localizations/lib && dart analyze --fatal-infos . \ No newline at end of file diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index bd4e36cb..20b3f60e 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -16,7 +16,7 @@ jobs: conventional_pr_title: runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v3.4.0 + - uses: amannn/action-semantic-pull-request@v5.2.0 with: scopes: | llc diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index e42174ad..99312316 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -8,6 +8,7 @@ on: pull_request: paths: - 'packages/**' + - '.github/workflows/stream_flutter_workflow.yml' types: - opened - reopened @@ -100,32 +101,32 @@ jobs: - name: "Collect Coverage" run: melos run coverage:ignore-file --no-select - name: "Upload Coverage" - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{secrets.CODECOV_TOKEN}} files: packages/*/coverage/lcov.info - name: "Stream Chat Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + uses: VeryGoodOpenSource/very_good_coverage@v2.1.0 with: path: packages/stream_chat/coverage/lcov.info min_coverage: 79 - - name: "Stream Chat Persistence Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + - name: "Stream Chat Localizations Coverage Check" + uses: VeryGoodOpenSource/very_good_coverage@v2.1.0 with: path: packages/stream_chat_localizations/coverage/lcov.info - min_coverage: 88 + min_coverage: 100 - name: "Stream Chat Persistence Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + uses: VeryGoodOpenSource/very_good_coverage@v2.1.0 with: path: packages/stream_chat_persistence/coverage/lcov.info min_coverage: 95 - name: "Stream Chat Flutter Core Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + uses: VeryGoodOpenSource/very_good_coverage@v2.1.0 with: path: packages/stream_chat_flutter_core/coverage/lcov.info min_coverage: 30 - name: "Stream Chat Flutter Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + uses: VeryGoodOpenSource/very_good_coverage@v2.1.0 with: path: packages/stream_chat_flutter/coverage/lcov.info min_coverage: 44 @@ -137,4 +138,4 @@ jobs: steps: - name: Run a one-line script - run: echo Draft PR, you are good. \ No newline at end of file + run: echo Draft PR, you are good. diff --git a/.gitignore b/.gitignore index 8f4fc629..c3f33953 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage .pub/ .dart_tool/ pubspec.lock +pubspec_overrides.yaml flutter_export_environment.sh examples/all_plugins/pubspec.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml index 6386f168..abb5d979 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,4 @@ analyzer: - plugins: - - dart_code_metrics exclude: - packages/*/lib/**/*.g.dart - packages/*/lib/scrollable_positioned_list/** @@ -159,23 +157,4 @@ linter: - unnecessary_overrides - prefer_null_aware_method_calls - use_named_constants - - use_raw_strings - -# https://dartcodemetrics.dev/docs/getting-started/introduction -dart_code_metrics: - rules: - # Dart Specific - - binary-expression-operand-order - - double-literal-format - - no-boolean-literal-compare - - no-equal-then-else - - no-empty-block: - exclude: - - packages/*/test/** - - prefer-trailing-comma: - exclude: - - packages/*/test/** - - # Flutter specific - - always-remove-listener - - avoid-unnecessary-setstate \ No newline at end of file + - use_raw_strings \ No newline at end of file diff --git a/docusaurus/docs/Flutter/05-guides/05-push-notifications/adding_push_notifications_v2.mdx b/docusaurus/docs/Flutter/05-guides/05-push-notifications/adding_push_notifications_v2.mdx index c0678e3b..c440daaf 100644 --- a/docusaurus/docs/Flutter/05-guides/05-push-notifications/adding_push_notifications_v2.mdx +++ b/docusaurus/docs/Flutter/05-guides/05-push-notifications/adding_push_notifications_v2.mdx @@ -40,12 +40,6 @@ Push message delivery behaves according to these rules: - The message doesn't contain the flag `skip_push` as true. - `push_notifications` is enabled (default) on the channel type for message is sent. -:::info - -If you would like get push notifications only when users are offline, please contact support. - -::: - :::caution Push notifications require membership. Watching a channel isn't enough. diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 573e21f8..6be503a3 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.5.0 + +🔄 Changed + +- Updated minimum supported `SDK` version to Dart 2.19 + ## 6.4.0 🐞 Fixed diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index b8a852eb..16362f02 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -5,18 +5,14 @@ publish_to: "none" version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' - flutter: ">=1.17.0" + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.7.0" dependencies: - cupertino_icons: ^1.0.0 + cupertino_icons: ^1.0.5 flutter: sdk: flutter - stream_chat: ^6.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter + stream_chat: ^6.4.0 flutter: uses-material-design: true diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index a2fb6fe6..6b1c214d 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 = '6.4.0'; +const PACKAGE_VERSION = '6.5.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 3bd6b49a..469f6b19 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,35 +1,34 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 6.4.0 +version: 6.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <4.0.0' + sdk: '>=2.19.0 <4.0.0' dependencies: async: ^2.10.0 collection: ^1.17.0 - dio: ^5.2.0 + dio: ^5.2.1+1 equatable: ^2.0.5 freezed_annotation: ^2.2.0 http_parser: ^4.0.2 jose: ^0.3.3 - json_annotation: ^4.8.0 - logging: ^1.1.1 + json_annotation: ^4.8.1 + logging: ^1.2.0 meta: ^1.8.0 mime: ^1.0.4 rate_limiter: ^1.0.0 rxdart: ^0.27.7 - synchronized: ^3.0.0 + synchronized: ^3.1.0 uuid: ^3.0.7 - web_socket_channel: ^2.3.0 + web_socket_channel: ^2.4.0 dev_dependencies: build_runner: ^2.3.3 - dart_code_metrics: ^5.7.2 - freezed: ^2.3.2 - json_serializable: ^6.6.1 + freezed: ^2.4.0 + json_serializable: ^6.6.2 mocktail: ^0.3.0 - test: ^1.24.1 + test: ^1.24.3 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 49b324d6..1263c4fe 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,10 @@ -## Upcoming +## 6.6.0 + +🔄 Changed + +- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19 + +## 6.5.0 🐞 Fixed @@ -6,6 +12,18 @@ after overriding the `onConfirmDeleteTap` callback. - [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error in `SendingIndicatorBuilder`. +- [[#1069]](https://github.com/GetStream/stream-chat-flutter/issues/1069) Fixed message swipe to reply using same + direction for both current user and other users. It now uses `SwipeDirection.startToEnd` for current user + and `SwipeDirection.endToStart` for other users. +- [[#1590]](https://github.com/GetStream/stream-chat-flutter/issues/1590) + Fixed `StreamMessageWidget.showReactionPickerIndicator` not toggling the reaction picker indicator visibility. +- [[#1639]](https://github.com/GetStream/stream-chat-flutter/issues/1639) Fixed attachments not showing in gallery view + even after saving them to the device. + > **Note** + > This fix depends on the [image_gallery_saver](https://pub.dev/packages/image_gallery_saver) plugin. Make sure to add + necessary permissions in your App as per the plugin documentation. +- [[#1642]](https://github.com/GetStream/stream-chat-flutter/issues/1642) Fixed `StreamMessageWidget.widthFactor` not + working on web and desktop platforms. ✅ Added @@ -26,6 +44,66 @@ 🔄 Changed - Deprecated `StreamMessageInput.attachmentThumbnailBuilders` in favor of `StreamMessageInput.mediaAttachmentBuilder`. +- Deprecated `StreamMessageListView.onMessageSwiped`. Try wrapping the `MessageWidget` with a `Swipeable`, `Dismissible` + or a custom widget to achieve the swipe to reply behaviour. + + ```dart + // Migration from onMessageSwiped to Swipeable. + StreamMessageListView( + ..., + messageBuilder: (context, messageDetails, messages, defaultWidget) { + // The threshold after which the message should be considered as swiped. + const threshold = 0.2; + + // The direction in which the message should be swiped to reply. + final swipeDirection = messageDetails.isMyMessage + ? SwipeDirection.endToStart // + : SwipeDirection.startToEnd; + + return Swipeable( + key: ValueKey(messageDetails.message.id), + direction: swipeDirection, + swipeThreshold: threshold, + onSwiped: (direction) { + // Handle the swipe action here. + }, + backgroundBuilder: (context, details) { + // The alignment of the swipe action. + final alignment = messageDetails.isMyMessage + ? Alignment.centerRight // + : Alignment.centerLeft; + + // The progress of the swipe action. + final progress = math.min(details.progress, threshold) / threshold; + + return Align( + alignment: alignment, + child: Opacity( + opacity: progress, + child: const Icon( + Icons.reply, + color: Colors.white, + ), + ), + ); + }, + child: defaultWidget, + ); + }, + ) + ``` +- Deprecated `StreamMessageWidget.showReactionPickerIndicator` in favor of `StreamMessageWidget.showReactionPicker`. + + ```diff + StreamMessageWidget( + - showReactionPickerIndicator: true/false, + + showReactionPicker: true/false, + ) + ``` +- Updated `video_player` dependency to `^2.7.0`. +- Updated `chewie` dependency to `^1.6.0`. +- Updated `share_plus` dependency to `^7.0.2`. +- Deprecated `StreamUserItem` in favor of `StreamUserListTile`. ## 6.4.0 @@ -109,21 +187,6 @@ ) ``` -- Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content insertion configuration. - [#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613) - - ```dart - StreamMessageInput( - ..., - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (content) { - // Do something with the content. - controller.addAttachment(...); - }, - ), - ) - ``` - ## 6.3.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index f0acc0fa..332d813b 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,6 +1,8 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:responsive_builder/responsive_builder.dart'; @@ -277,19 +279,84 @@ class _ChannelPageState extends State { children: [ Expanded( child: StreamMessageListView( - onMessageSwiped: - (CurrentPlatform.isAndroid || CurrentPlatform.isIos) - ? reply - : null, threadBuilder: (context, parent) { return ThreadPage( parent: parent!, ); }, - messageBuilder: - (context, details, messages, defaultWidget) { - return defaultWidget.copyWith( - onReplyTap: reply, + messageBuilder: ( + context, + messageDetails, + messages, + defaultWidget, + ) { + // The threshold after which the message is considered + // swiped. + const threshold = 0.2; + + final isMyMessage = messageDetails.isMyMessage; + + // The direction in which the message can be swiped. + final swipeDirection = isMyMessage + ? SwipeDirection.endToStart // + : SwipeDirection.startToEnd; + + return Swipeable( + key: ValueKey(messageDetails.message.id), + direction: swipeDirection, + swipeThreshold: threshold, + onSwiped: (details) => reply(messageDetails.message), + backgroundBuilder: (context, details) { + // The alignment of the swipe action. + final alignment = isMyMessage + ? Alignment.centerRight // + : Alignment.centerLeft; + + // The progress of the swipe action. + final progress = + math.min(details.progress, threshold) / threshold; + + // The offset for the reply icon. + var offset = Offset.lerp( + const Offset(-24, 0), + const Offset(12, 0), + progress, + )!; + + // If the message is mine, we need to flip the offset. + if (isMyMessage) { + offset = Offset(-offset.dx, -offset.dy); + } + + final _streamTheme = StreamChatTheme.of(context); + + return Align( + alignment: alignment, + child: Transform.translate( + offset: offset, + child: Opacity( + opacity: progress, + child: SizedBox.square( + dimension: 30, + child: CustomPaint( + painter: AnimatedCircleBorderPainter( + progress: progress, + color: _streamTheme.colorTheme.borders, + ), + child: Center( + child: StreamSvgIcon.reply( + size: lerpDouble(0, 18, progress), + color: _streamTheme + .colorTheme.accentPrimary, + ), + ), + ), + ), + ), + ), + ); + }, + child: defaultWidget.copyWith(onReplyTap: reply), ); }, ), diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 7e051f96..075e8132 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -1,87 +1,23 @@ name: stream_chat_flutter_example description: A new Flutter project. - -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' - flutter: ">=1.17.0" + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.7.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.4 flutter: sdk: flutter responsive_builder: ^0.7.0 - stream_chat_flutter: - path: ../ - stream_chat_localizations: - path: ../../stream_chat_localizations - stream_chat_persistence: - path: ../../stream_chat_persistence + stream_chat_flutter: ^6.4.0 + stream_chat_localizations: ^5.4.0 + stream_chat_persistence: ^6.4.0 -dev_dependencies: - flutter_test: - sdk: flutter - -dependency_overrides: - stream_chat_flutter: - path: ../ - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: assets: - assets/background_doodle.png - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart index 0b04ee61..e4d0b35e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:file_selector/file_selector.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; @@ -29,14 +30,16 @@ class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler { options: options, ); - // Open the native file browser so the user can select the download path. - final path = await getSavePath(suggestedName: data.fileName); - - if (path == null) { + // Open the native file browser so the user can select the save location. + final saveLocation = await getSaveLocation(suggestedName: data.fileName); + if (saveLocation == null) { // Operation was canceled by the user. return null; } + // Get the path to the user's selected location. + final path = saveLocation.path; + // Create an XFile for proper file saving. final file = data.toXFile(path: path); @@ -140,7 +143,6 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { final tempFilePath = tempPath .resolve(fileName!) .toFilePath(windows: CurrentPlatform.isWindows); - print(tempFilePath); final attachmentFileBytes = attachmentFile.bytes; if (attachmentFileBytes == null) { @@ -189,8 +191,26 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { // Create an XFile for proper file saving. final file = data.toXFile(path: path); - // Save the file to the user's selected path. + // Save the file to a temporary location. await file.saveTo(path); + + // Now that the file is saved, we need to copy it to the user's gallery + // because the gallery only shows files that are in the gallery folder. + await ImageGallerySaver.saveFile(path); + + // Once the file is copied to the gallery, we can delete the temporary file. + await file.delete(); + return path; } } + +extension on XFile { + /// Deletes this xfile from the file system. + Future delete() async { + final file = File(path); + if (file.existsSync()) { + await file.delete(); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart index a6feaa5e..2a791954 100644 --- a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart @@ -446,8 +446,12 @@ class VideoPackage { }) : _showControls = showControls, _autoInitialize = autoInitialize, _videoPlayerController = _attachment.localUri != null - ? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) - : VideoPlayerController.network(_attachment.assetUrl!); + ? VideoPlayerController.file( + File.fromUri(_attachment.localUri!), + ) + : VideoPlayerController.networkUrl( + Uri.parse(_attachment.assetUrl!), + ); final Attachment _attachment; final bool _showControls; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart index c70ed6c1..c9ff5595 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart @@ -15,7 +15,7 @@ class MessageActionsModal extends StatefulWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, + this.showReactionPicker = true, this.showDeleteMessage = true, this.showEditMessage = true, this.onReplyTap, @@ -54,8 +54,8 @@ class MessageActionsModal extends StatefulWidget { /// [StreamMessageThemeData] for message final StreamMessageThemeData messageTheme; - /// Flag for showing reactions - final bool showReactions; + /// Flag for showing reaction picker. + final bool showReactionPicker; /// Callback when copy is tapped final OnMessageTap? onCopyTap; @@ -103,6 +103,10 @@ class _MessageActionsModalState extends State { final user = StreamChat.of(context).currentUser; final orientation = mediaQueryData.orientation; + final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); + final fontSize = widget.messageTheme.messageTextStyle?.fontSize; final streamChatThemeData = StreamChatTheme.of(context); @@ -113,11 +117,10 @@ class _MessageActionsModalState extends State { child: Padding( padding: const EdgeInsets.all(8), child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.showReactions && widget.message.state.isCompleted) + if (widget.showReactionPicker && hasReactionPermission) LayoutBuilder( builder: (context, constraints) { return Align( diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index ab90710a..3bec1975 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -382,8 +382,9 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { @override void initState() { super.initState(); - _controller = VideoPlayerController.network(widget.attachment.assetUrl!) - ..initialize().then((_) { + _controller = VideoPlayerController.networkUrl( + Uri.parse(widget.attachment.assetUrl!), + )..initialize().then((_) { // ignore: no-empty-block setState(() {}); //when your thumbnail will show. }); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index 273e2c4d..ab36c3be 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -147,7 +147,6 @@ class StreamMessageInput extends StatefulWidget { _defaultClearQuotedMessageKeyPredicate, this.ogPreviewFilter = _defaultOgPreviewFilter, this.hintGetter = _defaultHintGetter, - this.contentInsertionConfiguration, }); /// The predicate used to send a message on desktop/web @@ -341,9 +340,6 @@ class StreamMessageInput extends StatefulWidget { /// Returns the hint text for the message input. final HintGetter hintGetter; - /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} - final ContentInsertionConfiguration? contentInsertionConfiguration; - static String? _defaultHintGetter( BuildContext context, HintType type, @@ -908,8 +904,6 @@ class StreamMessageInputState extends State decoration: _getInputDecoration(context), textCapitalization: widget.textCapitalization, autocorrect: widget.autoCorrect, - contentInsertionConfiguration: - widget.contentInsertionConfiguration, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart index a849bffc..46ebbd85 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart @@ -208,7 +208,7 @@ class MessageInputFileAttachments extends StatelessWidget { message: Message(), // dummy message attachment: attachment, constraints: BoxConstraints.loose(Size( - MediaQuery.sizeOf(context).width * 0.65, + MediaQuery.of(context).size.width * 0.65, 56, )), trailing: Padding( diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 292c9141..a04ab829 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -120,7 +120,6 @@ class StreamMessageTextField extends StatefulWidget { this.restorationId, this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - this.contentInsertionConfiguration, }) : assert(obscuringCharacter.length == 1, ''), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -527,9 +526,6 @@ class StreamMessageTextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; - /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} - final ContentInsertionConfiguration? contentInsertionConfiguration; - @override _StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); @@ -626,9 +622,6 @@ class StreamMessageTextField extends StatefulWidget { properties.add(DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); - properties.add(DiagnosticsProperty( - 'contentInsertionConfiguration', contentInsertionConfiguration, - defaultValue: null)); } } @@ -734,7 +727,6 @@ class _StreamMessageTextFieldState extends State restorationId: widget.restorationId, scribbleEnabled: widget.scribbleEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - contentInsertionConfiguration: widget.contentInsertionConfiguration, ); @override diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 8863c991..5a5075f8 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -1,5 +1,7 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -10,7 +12,6 @@ import 'package:stream_chat_flutter/src/message_list_view/loading_indicator.dart import 'package:stream_chat_flutter/src/message_list_view/mlv_utils.dart'; import 'package:stream_chat_flutter/src/message_list_view/thread_separator.dart'; import 'package:stream_chat_flutter/src/message_list_view/unread_messages_separator.dart'; -import 'package:stream_chat_flutter/src/misc/swipeable.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Spacing Types (These are properties of a message to help inform the decision @@ -96,6 +97,10 @@ class StreamMessageListView extends StatefulWidget { this.initialAlignment, this.scrollController, this.itemPositionListener, + @Deprecated( + 'Try wrapping the `MessageWidget` with a `Swipeable`, `Dismissible` or a ' + 'custom widget to achieve the swipe to reply behaviour.', + ) this.onMessageSwiped, this.highlightInitialMessage = false, this.messageHighlightColor, @@ -1226,26 +1231,6 @@ class _StreamMessageListViewState extends State { } var child = messageWidget; - if (!message.isDeleted && - !message.isSystem && - !message.isEphemeral && - widget.onMessageSwiped != null) { - child = Container( - decoration: const BoxDecoration(), - clipBehavior: Clip.hardEdge, - child: Swipeable( - onSwipeEnd: () { - FocusScope.of(context).unfocus(); - widget.onMessageSwiped?.call(message); - }, - backgroundIcon: StreamSvgIcon.reply( - color: _streamTheme.colorTheme.accentPrimary, - ), - child: child, - ), - ); - } - if (!initialMessageHighlightComplete && widget.highlightInitialMessage && isInitialMessage(message.id, streamChannel)) { @@ -1269,6 +1254,77 @@ class _StreamMessageListViewState extends State { ), ); } + + // Add swipeable if the callback is provided and the message is not deleted, + // system or ephemeral. + final onMessageSwiped = widget.onMessageSwiped; + if (onMessageSwiped != null && + !message.isDeleted && + !message.isSystem && + !message.isEphemeral) { + // The threshold after which the message is considered swiped. + const threshold = 0.2; + + // The direction in which the message can be swiped. + final swipeDirection = isMyMessage + ? SwipeDirection.endToStart // + : SwipeDirection.startToEnd; + + child = Swipeable( + key: ValueKey(message.id), + direction: swipeDirection, + swipeThreshold: threshold, + onSwiped: (_) => onMessageSwiped(message), + backgroundBuilder: (context, details) { + // The alignment of the swipe action. + final alignment = isMyMessage + ? Alignment.centerRight // + : Alignment.centerLeft; + + // The progress of the swipe action. + final progress = math.min(details.progress, threshold) / threshold; + + // The offset for the reply icon. + var offset = Offset.lerp( + const Offset(-24, 0), + const Offset(12, 0), + progress, + )!; + + // If the message is mine, we need to flip the offset. + if (isMyMessage) { + offset = Offset(-offset.dx, -offset.dy); + } + + return Align( + alignment: alignment, + child: Transform.translate( + offset: offset, + child: Opacity( + opacity: progress, + child: SizedBox.square( + dimension: 30, + child: CustomPaint( + painter: AnimatedCircleBorderPainter( + progress: progress, + color: _streamTheme.colorTheme.borders, + ), + child: Center( + child: StreamSvgIcon.reply( + size: lerpDouble(0, 18, progress), + color: _streamTheme.colorTheme.accentPrimary, + ), + ), + ), + ), + ), + ), + ); + }, + child: child, + ); + } + return child; } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart index 5cd93efc..9491b0da 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -188,19 +188,16 @@ class _MessageCardState extends State { attachmentPadding: widget.attachmentPadding, ), if (!widget.isGiphy) - ConstrainedBox( - constraints: BoxConstraints.loose(const Size.fromWidth(500)), - child: TextBubble( - messageTheme: widget.messageTheme, - message: widget.message, - textPadding: widget.textPadding, - textBuilder: widget.textBuilder, - isOnlyEmoji: widget.isOnlyEmoji, - hasQuotedMessage: widget.hasQuotedMessage, - hasUrlAttachments: widget.hasUrlAttachments, - onLinkTap: widget.onLinkTap, - onMentionTap: widget.onMentionTap, - ), + TextBubble( + messageTheme: widget.messageTheme, + message: widget.message, + textPadding: widget.textPadding, + textBuilder: widget.textBuilder, + isOnlyEmoji: widget.isOnlyEmoji, + hasQuotedMessage: widget.hasQuotedMessage, + hasUrlAttachments: widget.hasUrlAttachments, + onLinkTap: widget.onLinkTap, + onMentionTap: widget.onMentionTap, ), if (widget.hasUrlAttachments && !widget.hasQuotedMessage) _buildUrlAttachment(), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index 756aebc5..cb6be528 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -2,6 +2,7 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart' hide ButtonStyle; import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:meta/meta.dart'; import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart'; import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction_picker.dart'; @@ -54,7 +55,10 @@ class StreamMessageWidget extends StatefulWidget { this.attachmentBorderRadiusGeometry, this.onMentionTap, this.onMessageTap, - this.showReactionPickerIndicator = false, + bool? showReactionPicker, + @Deprecated('Use `showReactionPicker` instead') + bool showReactionPickerIndicator = true, + @internal this.showReactionPickerTail = false, this.showUserAvatar = DisplayWidget.show, this.showSendingIndicator = true, this.showThreadReplyIndicator = false, @@ -114,6 +118,7 @@ class StreamMessageWidget extends StatefulWidget { bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null, 'You can only use one of the two bottom row builders', ), + showReactionPicker = showReactionPicker ?? showReactionPickerIndicator, attachmentBuilders = { 'image': (context, message, attachments) { final border = RoundedRectangleBorder( @@ -460,10 +465,22 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final void Function(String)? onLinkTap; + /// {@template showReactionPicker} + /// Whether or not to show the reaction picker. + /// Used in [StreamMessageReactionsModal] and [MessageActionsModal]. + /// {@endtemplate} + final bool showReactionPicker; + /// {@template showReactionPickerIndicator} /// Used in [StreamMessageReactionsModal] and [MessageActionsModal] + /// {@endtemplate} @Deprecated('Use `showReactionPicker` instead') + bool get showReactionPickerIndicator => showReactionPicker; + + /// {@template showReactionPickerTail} + /// Whether or not to show the reaction picker tail /// {@endtemplate} - final bool showReactionPickerIndicator; + @internal + final bool showReactionPickerTail; /// {@template onShowMessage} /// Callback when show message is tapped @@ -619,7 +636,10 @@ class StreamMessageWidget extends StatefulWidget { bool? showInChannelIndicator, void Function(User)? onUserAvatarTap, void Function(String)? onLinkTap, + bool? showReactionPicker, + @Deprecated('Use `showReactionPicker` instead') bool? showReactionPickerIndicator, + @internal bool? showReactionPickerTail, List? readList, ShowMessageCallback? onShowMessage, bool? showUsername, @@ -703,8 +723,11 @@ class StreamMessageWidget extends StatefulWidget { showInChannelIndicator ?? this.showInChannelIndicator, onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, onLinkTap: onLinkTap ?? this.onLinkTap, - showReactionPickerIndicator: - showReactionPickerIndicator ?? this.showReactionPickerIndicator, + showReactionPicker: showReactionPicker ?? + showReactionPickerIndicator ?? + this.showReactionPicker, + showReactionPickerTail: + showReactionPickerTail ?? this.showReactionPickerTail, onShowMessage: onShowMessage ?? this.onShowMessage, showUsername: showUsername ?? this.showUsername, showTimestamp: showTimestamp ?? this.showTimestamp, @@ -883,12 +906,12 @@ class _StreamMessageWidgetState extends State if (!widget.message.state.isDeleted) { return ContextMenuArea( verticalPadding: 0, - builder: (context) => _buildContextMenu(), + builder: (_) => _buildContextMenu(), child: child, ); - } else { - return child; } + + return child; }, child: Material( type: MaterialType.transparency, @@ -961,9 +984,9 @@ class _StreamMessageWidgetState extends State messageWidget: widget, showBottomRow: showBottomRow, showPinHighlight: widget.showPinHighlight, - showReactionPickerIndicator: - widget.showReactionPickerIndicator, + showReactionPickerTail: widget.showReactionPickerTail, showReactions: showReactions, + onReactionsTap: () => _showMessageReactionsModal(context), showUserAvatar: widget.showUserAvatar, streamChat: _streamChat, translateUserAvatar: widget.translateUserAvatar, @@ -994,14 +1017,15 @@ class _StreamMessageWidgetState extends State final channel = StreamChannel.of(context).channel; return [ - StreamChatContextMenuItem( - child: StreamChannel( - channel: channel, - child: ContextMenuReactionPicker( - message: widget.message, + if (widget.showReactionPicker) + StreamChatContextMenuItem( + child: StreamChannel( + channel: channel, + child: ContextMenuReactionPicker( + message: widget.message, + ), ), ), - ), if (shouldShowReplyAction) ...[ StreamChatContextMenuItem( leading: StreamSvgIcon.reply(), @@ -1145,20 +1169,7 @@ class _StreamMessageWidgetState extends State ]; } - void onLongPress(BuildContext context) { - if (widget.message.isEphemeral || widget.message.state.isOutgoing) { - return; - } - - if (widget.onMessageActions != null) { - widget.onMessageActions!(context, widget.message); - } else { - _showMessageActionModalBottomSheet(context); - } - return; - } - - void _showMessageActionModalBottomSheet(BuildContext context) { + void _showMessageReactionsModal(BuildContext context) { final channel = StreamChannel.of(context).channel; showDialog( @@ -1168,7 +1179,8 @@ class _StreamMessageWidgetState extends State barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageReactionsModal( + showReactionPicker: widget.showReactionPicker, messageWidget: widget.copyWith( key: const Key('MessageWidget'), message: widget.message.copyWith( @@ -1182,37 +1194,92 @@ class _StreamMessageWidgetState extends State translateUserAvatar: false, showSendingIndicator: false, padding: EdgeInsets.zero, - showReactionPickerIndicator: - widget.showReactions && widget.message.state.isCompleted, + // Show the tail if the reaction picker is visible. + showReactionPickerTail: widget.showReactionPicker, showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id ? DisplayWidget.gone : DisplayWidget.show, ), - onCopyTap: (message) { - final text = message.text; - if (text != null) Clipboard.setData(ClipboardData(text: text)); - }, + onUserAvatarTap: widget.onUserAvatarTap, messageTheme: widget.messageTheme, reverse: widget.reverse, - showDeleteMessage: shouldShowDeleteAction, - onConfirmDeleteTap: widget.onConfirmDeleteTap, message: widget.message, - editMessageInputBuilder: widget.editMessageInputBuilder, - onReplyTap: widget.onReplyTap, - onThreadReplyTap: widget.onThreadTap, - showResendMessage: shouldShowResendAction, - showCopyMessage: shouldShowCopyAction, - showEditMessage: shouldShowEditAction, - showReactions: widget.showReactions, - showReplyMessage: shouldShowReplyAction, - showThreadReplyMessage: shouldShowThreadReplyAction, - showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, - customActions: widget.customActions, ), ), ); } + + void onLongPress(BuildContext context) { + if (widget.message.isEphemeral || widget.message.state.isOutgoing) { + return; + } + + if (widget.onMessageActions != null) { + return widget.onMessageActions!(context, widget.message); + } + + return _showMessageActionModalBottomSheet(context); + } + + void _showMessageActionModalBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + + showDialog( + useRootNavigator: false, + context: context, + useSafeArea: false, + barrierColor: _streamChatTheme.colorTheme.overlay, + builder: (context) { + return StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: widget.copyWith( + key: const Key('MessageWidget'), + message: widget.message.copyWith( + text: (widget.message.text?.length ?? 0) > 200 + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: EdgeInsets.zero, + // Show both the tail and indicator if the indicator is shown. + showReactionPickerTail: widget.showReactionPickerIndicator, + showPinHighlight: false, + showUserAvatar: widget.message.user!.id == + channel.client.state.currentUser!.id + ? DisplayWidget.gone + : DisplayWidget.show, + ), + onCopyTap: (message) { + final text = message.text; + if (text != null) Clipboard.setData(ClipboardData(text: text)); + }, + messageTheme: widget.messageTheme, + reverse: widget.reverse, + showDeleteMessage: shouldShowDeleteAction, + onConfirmDeleteTap: widget.onConfirmDeleteTap, + message: widget.message, + editMessageInputBuilder: widget.editMessageInputBuilder, + onReplyTap: widget.onReplyTap, + onThreadReplyTap: widget.onThreadTap, + showResendMessage: shouldShowResendAction, + showCopyMessage: shouldShowCopyAction, + showEditMessage: shouldShowEditAction, + showReactionPicker: widget.showReactionPickerIndicator, + showReplyMessage: shouldShowReplyAction, + showThreadReplyMessage: shouldShowThreadReplyAction, + showFlagButton: widget.showFlagButton, + showPinButton: widget.showPinButton, + customActions: widget.customActions, + ), + ); + }, + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index 92f475c2..2910ee62 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:meta/meta.dart'; import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -21,6 +22,7 @@ typedef BottomRowBuilderWithDefaultWidget = Widget Function( /// /// Should not be used outside of [MessageWidget. /// {@endtemplate} +@internal class MessageWidgetContent extends StatelessWidget { /// {@macro messageWidgetContent} const MessageWidgetContent({ @@ -33,6 +35,7 @@ class MessageWidgetContent extends StatelessWidget { required this.showUserAvatar, required this.avatarWidth, required this.showReactions, + required this.onReactionsTap, required this.messageTheme, required this.shouldShowReactions, required this.streamChatTheme, @@ -45,7 +48,7 @@ class MessageWidgetContent extends StatelessWidget { required this.attachmentBuilders, required this.attachmentPadding, required this.textPadding, - required this.showReactionPickerIndicator, + required this.showReactionPickerTail, required this.translateUserAvatar, required this.bottomRowPadding, required this.showInChannel, @@ -111,6 +114,11 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro showReactions} final bool showReactions; + /// Callback called when the reactions icon is tapped. + /// + /// Do not confuse this with the tap action on the reactions picker. + final VoidCallback onReactionsTap; + /// {@macro messageTheme} final StreamMessageThemeData messageTheme; @@ -174,8 +182,8 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro quotedMessageBuilder} final Widget Function(BuildContext, Message)? quotedMessageBuilder; - /// {@macro showReactionPickerIndicator} - final bool showReactionPickerIndicator; + /// {@macro showReactionPickerTail} + final bool showReactionPickerTail; /// {@macro translateUserAvatar} final bool translateUserAvatar; @@ -288,9 +296,7 @@ class MessageWidgetContent extends StatelessWidget { ownId: streamChat.currentUser!.id, reverse: reverse, shouldShowReactions: shouldShowReactions, - onTap: () => _showMessageReactionsModal( - context, - ), + onTap: onReactionsTap, ) : null, anchor: Aligned( @@ -363,7 +369,8 @@ class MessageWidgetContent extends StatelessWidget { shape: shape, ), ), - if (showReactionPickerIndicator) + // TODO: Make tail part of the Reaction Picker. + if (showReactionPickerTail) Positioned( right: reverse ? null : 4, left: reverse ? 4 : null, @@ -374,6 +381,7 @@ class MessageWidgetContent extends StatelessWidget { Colors.transparent, Colors.transparent, tailCirclesSpace: 1, + flipTail: !reverse, ), ), ), @@ -434,47 +442,6 @@ class MessageWidgetContent extends StatelessWidget { ); } - void _showMessageReactionsModal(BuildContext context) { - final channel = StreamChannel.of(context).channel; - showDialog( - useRootNavigator: false, - context: context, - useSafeArea: false, - barrierColor: streamChatTheme.colorTheme.overlay, - builder: (context) => StreamChannel( - channel: channel, - child: StreamMessageReactionsModal( - messageWidget: messageWidget.copyWith( - key: const Key('MessageWidget'), - message: message.copyWith( - text: (message.text?.length ?? 0) > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), - showReactions: false, - showUsername: false, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - padding: EdgeInsets.zero, - showReactionPickerIndicator: - showReactions && message.state.isCompleted, - showPinHighlight: false, - showUserAvatar: - message.user!.id == channel.client.state.currentUser!.id - ? DisplayWidget.gone - : DisplayWidget.show, - ), - onUserAvatarTap: onUserAvatarTap, - messageTheme: messageTheme, - reverse: reverse, - message: message, - showReactions: showReactions, - ), - ), - ); - } - Widget _buildBottomRow(BuildContext context) { final defaultWidget = BottomRow( message: message, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index 928a8414..8dadc47d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -15,7 +15,7 @@ class StreamMessageReactionsModal extends StatelessWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions, + this.showReactionPicker = true, this.reverse = false, this.onUserAvatarTap, }); @@ -32,8 +32,8 @@ class StreamMessageReactionsModal extends StatelessWidget { /// {@macro reverse} final bool reverse; - /// {@macro showReactions} - final bool? showReactions; + /// Flag for showing reaction picker. + final bool showReactionPicker; /// {@macro onUserAvatarTap} final void Function(User)? onUserAvatarTap; @@ -56,8 +56,7 @@ class StreamMessageReactionsModal extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if ((showReactions ?? hasReactionPermission) && - message.state.isCompleted) + if (showReactionPicker && hasReactionPermission) LayoutBuilder( builder: (context, constraints) { return Align( diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart index e4e1ad48..a7baad7e 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart @@ -43,19 +43,9 @@ double calculateReactionsHorizontalAlignment( final signal = user?.id == message.user?.id ? 1 : -1; final result = signal * (1 - divFactor * 2.0); - return _capResult(result); -} - -// Ensure reactions don't get pushed past the edge of the screen. -// -// This happens if divFactor is really big. When this happens, we can simply -// move the model all the way to the end of screen. -double _capResult(double result) { - if (result > 1.0) { - return 1; - } else if (result < -1.0) { - return -1; - } else { - return result; - } + // Ensure reactions don't get pushed past the edge of the screen. + // + // This happens if divFactor is really big. When this happens, we can simply + // move the model all the way to the end of screen. + return result.clamp(-1, 1); } diff --git a/packages/stream_chat_flutter/lib/src/misc/animated_circle_border_painter.dart b/packages/stream_chat_flutter/lib/src/misc/animated_circle_border_painter.dart new file mode 100644 index 00000000..bd02678f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/animated_circle_border_painter.dart @@ -0,0 +1,72 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; + +/// A custom painter that animates a circle border or fills it based on a +/// progress value. +/// +/// This painter draws a circle with a border that can be animated to fill the +/// circle or stroke its outline based on a given progress value. The progress +/// value is a double between 0.0 and 1.0, representing the completion of the +/// animation. +/// +/// When the progress is 0.0, the circle is completely empty, and when the +/// progress is 1.0, the circle is fully filled or stroked. +/// +/// The color of the arc/circle can be customized by providing a [color]. +/// +/// Example usage: +/// ```dart +/// AnimatedCircleBorderPainter painter = AnimatedCircleBorderPainter( +/// progress: 0.5, +/// color: Colors.blue, +/// ); +/// +/// CustomPaint( +/// painter: painter, +/// size: Size(200, 200), +/// // ... other properties +/// ) +/// ``` +class AnimatedCircleBorderPainter extends CustomPainter { + /// Creates an [AnimatedCircleBorderPainter] with the specified [progress] + /// and [color]. + const AnimatedCircleBorderPainter({ + required this.progress, + required this.color, + }); + + /// The progress of the animation, as a value between 0.0 and 1.0. + final double progress; + + /// The color of the arc/circle. + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final style = progress == 1.0 ? PaintingStyle.fill : PaintingStyle.stroke; + + final arcPaint = Paint() + ..style = style + ..color = color + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + + final radius = size.width / 2; + final center = Offset(size.width / 2, size.height / 2); + final sweepAngle = math.pi * 2 * progress; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + -math.pi / 2, + sweepAngle, + false, + arcPaint, + ); + } + + @override + bool shouldRepaint(AnimatedCircleBorderPainter oldPainter) { + return oldPainter.progress != progress || oldPainter.color != color; + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/swipeable.dart b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart index ded8520b..45b9b2dc 100644 --- a/packages/stream_chat_flutter/lib/src/misc/swipeable.dart +++ b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart @@ -1,173 +1,464 @@ import 'dart:math' as math; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template swipeable} -/// A swipeable tile in a list. Swiping on the tile will reveal actions that -/// can be taken. -/// {@endtemplate} -class Swipeable extends StatefulWidget { - /// {@macro swipeable} - const Swipeable({ - super.key, - required this.child, - required this.backgroundIcon, - this.onSwipeStart, - this.onSwipeCancel, - this.onSwipeEnd, - this.threshold = 82.0, - }); +/// Signature used by [swipeable] to indicate that it has been swiped in +/// the given `direction`. +/// +/// Used by [Swipeable.onSwiped]. +typedef SwipeDirectionCallback = void Function(SwipeDirection direction); - /// Child to make swipeable - final Widget child; +/// Signature for a function that builds a widget given the progress of the +/// dismissing action. +/// +/// Used by [Swipeable.backgroundBuilder]. +typedef BackgroundWidgetBuilder = Widget Function( + BuildContext context, + SwipeUpdateDetails details, +); - /// Background icon after swipe - final Widget backgroundIcon; +/// The direction in which a [Swipeable] can be swiped. +enum SwipeDirection { + /// The [Swipeable] can be swiped by dragging either left or right. + horizontal, - /// The action to perform when the swipe starts - final VoidCallback? onSwipeStart; + /// The [Swipeable] can be swiped by dragging in the reverse of the + /// reading direction (e.g., from right to left in left-to-right languages). + endToStart, - /// The action to perform when the swipe is cancelled - final VoidCallback? onSwipeCancel; + /// The [Swipeable] can be swiped by dragging in the reading direction + /// (e.g., from left to right in left-to-right languages). + startToEnd, - /// The action to perform when the swipe ends - final VoidCallback? onSwipeEnd; - - /// Threshold for swipe - final double threshold; - - @override - State createState() => _SwipeableState(); + /// The [Swipeable] cannot be swiped by dragging. + none } -class _SwipeableState extends State with TickerProviderStateMixin { - double _dragExtent = 0; - late AnimationController _moveController; - late AnimationController _iconMoveController; - late Animation _moveAnimation; - late Animation _iconTransitionAnimation; - late Animation _iconFadeAnimation; - bool _pastThreshold = false; +/// A widget that can be swiped in a specified direction. +/// +/// The `Swipeable` widget allows its child to be swiped by the user in a +/// specified direction. +/// +/// It provides options for customizing the swipe behavior, including the +/// ability to specify a background widget that appears during the swipe, +/// callbacks for handling swipe completion, and more. +/// +/// Example usage: +/// ```dart +/// Swipeable( +/// child: Container( +/// height: 100, +/// width: 200, +/// color: Colors.blue, +/// child: Center( +/// child: Text('Swipe me'), +/// ), +/// ), +/// backgroundBuilder: (context, details) { +/// final direction = details.direction; +/// return Container( +/// color: Colors.red, +/// child: Center( +/// child: Text( +/// direction == SwipeDirection.left ? 'Swipe left' : 'Swipe right', +/// style: TextStyle( +/// color: Colors.white, +/// fontWeight: FontWeight.bold, +/// ), +/// ), +/// ), +/// ); +/// }, +/// onSwiped: (direction) { +/// if (direction == SwipeDirection.left) { +/// // Handle left swipe +/// } else if (direction == SwipeDirection.right) { +/// // Handle right swipe +/// } +/// }, +/// direction: SwipeDirection.horizontal, +/// swipeThreshold: 0.4, +/// movementDuration: Duration(milliseconds: 200), +/// ) +/// ``` +class Swipeable extends StatefulWidget { + /// Creates a widget that can be swiped . + const Swipeable({ + required super.key, + required this.child, + this.backgroundBuilder, + this.onSwiped, + this.direction = SwipeDirection.horizontal, + this.swipeThreshold = 0.4, + this.movementDuration = const Duration(milliseconds: 200), + this.dragStartBehavior = DragStartBehavior.start, + this.behavior = HitTestBehavior.opaque, + }) : assert( + swipeThreshold >= 0.0 && swipeThreshold <= 1.0, + 'swipeThreshold must be between 0.0 and 1.0', + ); - final _animationDuration = const Duration(milliseconds: 200); + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + /// A widget that is stacked behind the child. If secondaryBackground is also + /// specified then this widget only appears when the child has been dragged + /// down or to the right. + final BackgroundWidgetBuilder? backgroundBuilder; + + /// Called when the widget has been successfully swiped based on the + /// [direction] and [swipeThreshold]. + final SwipeDirectionCallback? onSwiped; + + /// The direction in which the widget can be swiped. + final SwipeDirection direction; + + /// The offset threshold the item has to be dragged in order to be considered + /// swiped. + /// + /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item + /// has to be dragged at least 40% towards one direction to be considered + /// swiped. + /// + /// See also: + /// + /// * [direction], which controls the directions in which the items can + /// be swiped. + final double swipeThreshold; + + /// Defines the duration for card to come back to original position. + final Duration movementDuration; + + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a + /// swipeable will begin at the position where the drag gesture won the + /// arena. + /// + /// If set to [DragStartBehavior.down] it will begin at the position where + /// a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for + /// the different behaviors. + final DragStartBehavior dragStartBehavior; + + /// How to behave during hit tests. + /// + /// This defaults to [HitTestBehavior.opaque]. + final HitTestBehavior behavior; + + @override + _SwipeableState createState() => _SwipeableState(); +} + +/// Details for [DismissUpdateCallback]. +/// +/// See also: +/// +/// * [swipeable.onUpdate], which receives this information. +class SwipeUpdateDetails { + /// Create a new instance of [SwipeUpdateDetails]. + SwipeUpdateDetails({ + this.direction = SwipeDirection.horizontal, + this.reached = false, + this.previousReached = false, + this.progress = 0.0, + }); + + /// The direction that the swipeable is being dragged. + final SwipeDirection direction; + + /// Whether the swipe threshold is currently reached. + final bool reached; + + /// Whether the swipe threshold was reached the last time this callback was + /// invoked. + /// + /// This can be used in conjunction with [SwipeUpdateDetails.reached] to catch + /// the moment that the [Swipeable] is dragged across the threshold. + final bool previousReached; + + /// The offset ratio of the swipeable in its parent container. + /// + /// A value of 0.0 represents the normal position and 1.0 means the child is + /// completely outside its parent. + /// + /// This can be used to synchronize other elements to what the swipeable is + /// doing on screen, e.g. using this value to set the opacity thereby fading + /// swipeable as it's dragged offscreen. + final double progress; +} + +class _SwipeableClipper extends CustomClipper { + _SwipeableClipper({ + required this.moveAnimation, + }) : super(reclip: moveAnimation); + + final Animation moveAnimation; + + @override + Rect getClip(Size size) { + final offset = moveAnimation.value.dx * size.width; + if (offset < 0) { + return Rect.fromLTRB(size.width + offset, 0, size.width, size.height); + } + return Rect.fromLTRB(0, 0, offset, size.height); + } + + @override + Rect getApproximateClipRect(Size size) => getClip(size); + + @override + bool shouldReclip(_SwipeableClipper oldClipper) { + return oldClipper.moveAnimation.value != moveAnimation.value; + } +} + +class _SwipeableState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - _moveController = - AnimationController(duration: _animationDuration, vsync: this); - _iconMoveController = - AnimationController(duration: _animationDuration, vsync: this); - _moveAnimation = Tween(begin: Offset.zero, end: const Offset(1, 0)) - .animate(_moveController); - _iconTransitionAnimation = - Tween(begin: const Offset(-0.1, 0), end: const Offset(0.4, 0)) - .animate(_moveController); - _iconFadeAnimation = - Tween(begin: 0.7, end: 1).animate(_iconMoveController); - - const controllerValue = 0.0; - _moveController.animateTo(controllerValue); - _iconMoveController.animateTo(controllerValue); + _moveController = AnimationController( + duration: widget.movementDuration, + vsync: this, + )..addStatusListener((_) => updateKeepAlive()); + _updateMoveAnimation(); } + AnimationController? _moveController; + late Animation _moveAnimation; + + double _dragExtent = 0; + bool _dragUnderway = false; + bool _swipeThresholdReached = false; + + final GlobalKey _contentKey = GlobalKey(); + + @override + bool get wantKeepAlive => _moveController?.isAnimating ?? false; + @override void dispose() { - _moveController.dispose(); - _iconMoveController.dispose(); + _moveController?.dispose(); super.dispose(); } + SwipeDirection _extentToDirection(double extent) { + if (extent == 0.0) { + return SwipeDirection.none; + } + switch (Directionality.of(context)) { + case TextDirection.rtl: + return extent < 0 + ? SwipeDirection.startToEnd + : SwipeDirection.endToStart; + case TextDirection.ltr: + return extent > 0 + ? SwipeDirection.startToEnd + : SwipeDirection.endToStart; + } + } + + SwipeDirection get _swipeDirection => _extentToDirection(_dragExtent); + + bool get _isActive => _dragUnderway || _moveController!.isAnimating; + + double get _overallDragAxisExtent => context.size!.width; + void _handleDragStart(DragStartDetails details) { - widget.onSwipeStart?.call(); + _dragUnderway = true; + if (_moveController!.isAnimating) { + _dragExtent = + _moveController!.value * _overallDragAxisExtent * _dragExtent.sign; + _moveController!.stop(); + } else { + _dragExtent = 0.0; + _moveController!.value = 0.0; + } + setState(_updateMoveAnimation); } void _handleDragUpdate(DragUpdateDetails details) { + if (!_isActive || _moveController!.isAnimating) { + return; + } + final delta = details.primaryDelta!; - _dragExtent += delta; - - if (_dragExtent.isNegative) return; - - final movePastThresholdPixels = widget.threshold; - var newPos = _dragExtent.abs() / context.size!.width; - - if (_dragExtent.abs() > movePastThresholdPixels) { - // how many "thresholds" past the threshold we are. 1 = the threshold 2 - // = two thresholds. - final n = _dragExtent.abs() / movePastThresholdPixels; - - // Take the number of thresholds past the threshold, and reduce this - // number - final reducedThreshold = math.pow(n, 0.3); - - final adjustedPixelPos = movePastThresholdPixels * reducedThreshold; - newPos = adjustedPixelPos / context.size!.width; - - if (_dragExtent > 0 && !_pastThreshold) { - _iconMoveController.value = 1; - _pastThreshold = true; - } - } else { - // Send a cancel event if the user has swiped back underneath the - // threshold - if (_pastThreshold && widget.onSwipeCancel != null) { - widget.onSwipeCancel!(); - } - _pastThreshold = false; + final oldDragExtent = _dragExtent; + switch (widget.direction) { + case SwipeDirection.horizontal: + _dragExtent += delta; + break; + case SwipeDirection.endToStart: + switch (Directionality.of(context)) { + case TextDirection.rtl: + if (_dragExtent + delta > 0) { + _dragExtent += delta; + } + break; + case TextDirection.ltr: + if (_dragExtent + delta < 0) { + _dragExtent += delta; + } + break; + } + break; + case SwipeDirection.startToEnd: + switch (Directionality.of(context)) { + case TextDirection.rtl: + if (_dragExtent + delta < 0) { + _dragExtent += delta; + } + break; + case TextDirection.ltr: + if (_dragExtent + delta > 0) { + _dragExtent += delta; + } + break; + } + break; + case SwipeDirection.none: + _dragExtent = 0; + break; } - if (!_pastThreshold || newPos < _moveController.value) { - _iconMoveController.value = newPos; + + if (oldDragExtent.sign != _dragExtent.sign) { + setState(_updateMoveAnimation); } - _moveController.value = newPos; + + if (!_moveController!.isAnimating) { + final currentDragExtent = _dragExtent.abs(); + final overallDragExtent = _overallDragAxisExtent; + final movePastThresholdExtent = widget.swipeThreshold * overallDragExtent; + + final double newPos; + if (currentDragExtent > movePastThresholdExtent) { + // How many "thresholds" past the threshold we are. + final n = currentDragExtent / movePastThresholdExtent; + + // Take the number of thresholds past the threshold, and reduce it by + // the threshold amount, then normalize it to the drag extents. + final reducedThreshold = math.pow(n, 0.3); + final adjustedDragExtent = movePastThresholdExtent * reducedThreshold; + + newPos = adjustedDragExtent / overallDragExtent; + } else { + newPos = currentDragExtent / overallDragExtent; + } + + _moveController!.value = newPos; + } + } + + SwipeUpdateDetails _currentSwipeUpdateDetails() { + final oldSwipeThresholdReached = _swipeThresholdReached; + _swipeThresholdReached = _moveController!.value > widget.swipeThreshold; + + return SwipeUpdateDetails( + direction: _swipeDirection, + reached: _swipeThresholdReached, + previousReached: oldSwipeThresholdReached, + progress: _moveController!.value, + ); + } + + void _updateMoveAnimation() { + final end = _dragExtent.sign; + _moveAnimation = _moveController!.drive( + Tween( + begin: Offset.zero, + end: Offset(end, 0), + ), + ); } void _handleDragEnd(DragEndDetails details) { - _moveController.animateTo(0, duration: _animationDuration); - _iconMoveController.animateTo(0, duration: _animationDuration); - _dragExtent = 0.0; - if (_pastThreshold && widget.onSwipeEnd != null) { - widget.onSwipeEnd!(); + if (!_isActive || _moveController!.isAnimating) return; + _dragUnderway = false; + + // Once dragging ends, animate back to the initial offset. + _moveController!.reverse(); + + // If the threshold was reached, report it. + if (_moveController!.value > widget.swipeThreshold) { + if (widget.onSwiped != null) { + final direction = _swipeDirection; + widget.onSwiped!(direction); + } } } @override Widget build(BuildContext context) { + super.build(context); // See AutomaticKeepAliveClientMixin. + + assert( + debugCheckHasDirectionality(context), + 'Swipeable must be inside of a Directionality widget.', + ); + + Widget? background; + final backgroundBuilder = widget.backgroundBuilder; + if (backgroundBuilder != null) { + background = AnimatedBuilder( + animation: _moveAnimation, + builder: (context, _) { + final updateDetails = _currentSwipeUpdateDetails(); + return backgroundBuilder.call(context, updateDetails); + }, + ); + } + + Widget content = SlideTransition( + position: _moveAnimation, + child: KeyedSubtree(key: _contentKey, child: widget.child), + ); + + if (background != null) { + content = Stack( + fit: StackFit.passthrough, + alignment: Alignment.center, + children: [ + if (!_moveAnimation.isDismissed) + Positioned.fill( + child: ClipRect( + clipper: _SwipeableClipper( + moveAnimation: _moveAnimation, + ), + child: background, + ), + ), + content, + ], + ); + } + + // If the SwipeDirection is none, we do not add drag gestures because the + // content cannot be dragged. + if (widget.direction == SwipeDirection.none) { + return content; + } + + // We are not resizing but we may be being dragging in widget.direction. return GestureDetector( onHorizontalDragStart: _handleDragStart, onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragEnd: _handleDragEnd, - behavior: HitTestBehavior.opaque, - child: Stack( - alignment: Alignment.center, - fit: StackFit.passthrough, - children: [ - SlideTransition( - position: _iconTransitionAnimation, - child: Row( - children: [ - FadeTransition( - opacity: _iconFadeAnimation, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: StreamChatTheme.of(context).colorTheme.disabled, - ), - ), - child: widget.backgroundIcon, - ), - ), - ], - ), - ), - SlideTransition( - position: _moveAnimation, - child: widget.child, - ), - ], - ), + behavior: widget.behavior, + dragStartBehavior: widget.dragStartBehavior, + child: content, ); } } diff --git a/packages/stream_chat_flutter/lib/src/user/user_item.dart b/packages/stream_chat_flutter/lib/src/user/user_item.dart index 85d12a99..019322d9 100644 --- a/packages/stream_chat_flutter/lib/src/user/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user/user_item.dart @@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// [StreamChatTheme]. /// Modify it to change the widget's appearance. /// {@endtemplate} +@Deprecated('Use `StreamUserListTile` instead.') class StreamUserItem extends StatelessWidget { /// {@macro streamUserItem} const StreamUserItem({ diff --git a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart index 17cab820..64f18e1a 100644 --- a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart +++ b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart @@ -88,7 +88,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?); typedef ChannelInfoCallback = void Function(Channel); /// {@template channelPreviewBuilder} -/// Builder used to create a custom [ChannelPreview] for a [Channel] +/// Builder used to create a custom ChannelPreview for a [Channel] /// {@endtemplate} @Deprecated('Use StreamChannelListViewIndexedWidgetBuilder instead') typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index e5ca5a1e..50977784 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -58,6 +58,7 @@ export 'src/message_widget/message_text.dart'; export 'src/message_widget/message_widget.dart'; export 'src/message_widget/reactions/reaction_picker.dart'; export 'src/message_widget/text_bubble.dart'; +export 'src/misc/animated_circle_border_painter.dart'; export 'src/misc/back_button.dart'; export 'src/misc/connection_status_builder.dart'; export 'src/misc/date_divider.dart'; @@ -66,6 +67,7 @@ export 'src/misc/option_list_tile.dart'; export 'src/misc/reaction_icon.dart'; export 'src/misc/stream_neumorphic_button.dart'; export 'src/misc/stream_svg_icon.dart'; +export 'src/misc/swipeable.dart'; export 'src/misc/system_message.dart'; export 'src/misc/thread_header.dart'; export 'src/misc/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 45f43a5f..f120f931 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,50 +1,50 @@ 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: 6.4.0 +version: 6.6.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=1.20.0" + sdk: ">=2.19.0 <4.0.0" + flutter: ">=3.7.0" dependencies: - cached_network_image: ^3.0.0 - chewie: ^1.3.4 + cached_network_image: ^3.2.3 + chewie: ^1.7.0 collection: ^1.17.0 contextmenu: ^3.0.0 dart_vlc: ^0.4.0 - desktop_drop: ^0.4.0 - diacritic: ^0.1.3 - dio: ^5.2.0 + desktop_drop: ^0.4.1 + diacritic: ^0.1.4 + dio: ^5.2.1+1 ezanimation: ^0.6.0 - file_picker: ^5.2.4 - file_selector: ^0.9.0 + file_picker: ^5.3.1 + file_selector: ^1.0.0 flutter: sdk: flutter - flutter_markdown: ^0.6.1 - flutter_portal: ^1.0.0 - flutter_svg: ^2.0.4 - http_parser: ^4.0.0 - image_gallery_saver: ^2.0.1 - image_picker: ^0.8.2 + flutter_markdown: ^0.6.15 + flutter_portal: ^1.1.4 + flutter_svg: ^2.0.5 + http_parser: ^4.0.2 + image_gallery_saver: ^2.0.3 + image_picker: ^1.0.0 jiffy: ^5.0.0 - lottie: ^2.0.0 + lottie: ^2.3.2 meta: ^1.8.0 - path_provider: ^2.0.9 - photo_manager: ^2.5.2 + path_provider: ^2.0.15 + photo_manager: ^2.6.0 photo_view: ^0.14.0 - rxdart: ^0.27.0 - share_plus: ^6.3.0 + rxdart: ^0.27.7 + share_plus: ^7.0.2 shimmer: ^3.0.0 - stream_chat_flutter_core: ^6.4.0 - synchronized: ^3.0.0 + stream_chat_flutter_core: ^6.5.0 + synchronized: ^3.1.0 thumblr: ^0.0.4 - url_launcher: ^6.1.0 - video_player: ^2.4.5 + url_launcher: ^6.1.11 + video_player: ^2.7.0 video_player_macos: ^2.0.1 - video_thumbnail: ^0.5.0 + video_thumbnail: ^0.5.3 flutter: plugin: @@ -70,9 +70,8 @@ flutter: uses-material-design: true dev_dependencies: - dart_code_metrics: ^5.7.2 flutter_test: sdk: flutter golden_toolkit: ^0.15.0 mocktail: ^0.3.0 - path: ^1.8.2 + path: ^1.8.2 \ No newline at end of file diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart new file mode 100644 index 00000000..a7afeea8 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_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 '../mocks.dart'; + +Widget wrapWithStreamChat( + Widget child, { + StreamChatClient? client, +}) { + return MaterialApp( + home: StreamChat( + client: client ?? MockClient(), + child: child, + ), + ); +} + +void main() { + group('StreamMessageInputAttachmentList tests', () { + testWidgets( + 'StreamMessageInputAttachmentList should render attachments', + (WidgetTester tester) async { + final attachments = [ + Attachment(type: 'file', id: 'file1'), + Attachment(type: 'file', id: 'file2'), + Attachment(type: 'media', id: 'media1'), + ]; + + await tester.pumpWidget( + wrapWithStreamChat( + StreamMessageInputAttachmentList( + attachments: attachments, + ), + ), + ); + + // Expect 2 file attachments and 1 media attachment + expect(find.byType(MessageInputFileAttachments), findsOneWidget); + expect(find.byType(MessageInputMediaAttachments), findsOneWidget); + }, + ); + + testWidgets( + 'StreamMessageInputAttachmentList should call onRemovePressed callback', + (WidgetTester tester) async { + Attachment? removedAttachment; + + final attachments = [ + Attachment(type: 'file', id: 'file1'), + Attachment(type: 'file', id: 'file2'), + ]; + + await tester.pumpWidget( + wrapWithStreamChat( + StreamMessageInputAttachmentList( + attachments: attachments, + onRemovePressed: (attachment) { + removedAttachment = attachment; + }, + ), + ), + ); + + final removeButtons = find.byType(RemoveAttachmentButton); + + // Tap the first remove button + await tester.tap(removeButtons.first); + await tester.pump(); + + // Expect the onRemovePressed callback to be called with the second + // attachment as they are reversed in the UI. + expect(removedAttachment, attachments[1]); + }, + ); + + testWidgets( + '''StreamMessageInputAttachmentList should display empty box if no attachments''', + (WidgetTester tester) async { + final attachments = []; + + await tester.pumpWidget( + wrapWithStreamChat( + StreamMessageInputAttachmentList( + attachments: attachments, + ), + ), + ); + + // Expect an empty box + expect(find.byType(SizedBox), findsOneWidget); + }, + ); + }); + + group('MessageInputFileAttachments tests', () { + testWidgets( + 'MessageInputFileAttachments should render file attachments', + (WidgetTester tester) async { + final attachments = [ + Attachment(type: 'file', id: 'file1'), + Attachment(type: 'file', id: 'file2'), + ]; + + await tester.pumpWidget( + wrapWithStreamChat( + MessageInputFileAttachments( + attachments: attachments, + ), + ), + ); + + // Expect 2 file attachments + expect(find.byType(ClipRRect), findsNWidgets(2)); + }, + ); + + testWidgets( + 'MessageInputFileAttachments should call onRemovePressed callback', + (WidgetTester tester) async { + Attachment? removedAttachment; + + final attachments = [ + Attachment(type: 'file', id: 'file1'), + ]; + + await tester.pumpWidget( + wrapWithStreamChat( + MessageInputFileAttachments( + attachments: attachments, + onRemovePressed: (attachment) { + removedAttachment = attachment; + }, + ), + ), + ); + + final removeButton = find.byType(RemoveAttachmentButton); + + // Tap the remove button + await tester.tap(removeButton); + await tester.pump(); + + // Expect the onRemovePressed callback to be called with the attachment + expect(removedAttachment, attachments.first); + }, + ); + }); + + group('MessageInputMediaAttachments tests', () { + testWidgets( + 'MessageInputMediaAttachments should render media attachments', + (WidgetTester tester) async { + final attachments = [ + Attachment(type: 'media', id: 'media1'), + Attachment(type: 'media', id: 'media2'), + ]; + + await tester.pumpWidget( + wrapWithStreamChat( + MessageInputMediaAttachments( + attachments: attachments, + ), + ), + ); + + // Expect 2 media attachments + expect(find.byType(Stack), findsNWidgets(2)); + }, + ); + + testWidgets( + 'MessageInputMediaAttachments should display empty box if no attachments', + (WidgetTester tester) async { + final attachments = []; + + await tester.pumpWidget( + wrapWithStreamChat( + MessageInputMediaAttachments( + attachments: attachments, + ), + ), + ); + + // Expect an empty box + expect(find.byType(SizedBox), findsOneWidget); + }, + ); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart index 54f3675b..20ecd58e 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart @@ -104,7 +104,7 @@ void main() { message: message, messageTheme: streamTheme.ownMessageTheme, reverse: true, - showReactions: false, + showReactionPicker: false, onUserAvatarTap: onUserAvatarTap, ), ), diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index dbab9611..edb10b9a 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.5.0 + +- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19 + ## 6.4.0 - Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index 0abc0cdc..c92a4fc6 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -1,76 +1,17 @@ name: stream_chat_flutter_core_example description: Example app for testing stream_chat_flutter_core - -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' - flutter: ">=1.17.0" + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.7.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: ^5.1.0 + stream_chat_flutter_core: ^6.4.0 -dev_dependencies: - flutter_test: - sdk: flutter - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 852b0afc..5f30475c 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,30 +1,29 @@ 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: 6.4.0 +version: 6.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <4.0.0' - flutter: ">=1.17.0" + sdk: ">=2.19.0 <4.0.0" + flutter: ">=3.7.0" dependencies: collection: ^1.17.0 - connectivity_plus: ^4.0.0 + connectivity_plus: ^4.0.1 flutter: sdk: flutter - freezed_annotation: ^2.0.3 + freezed_annotation: ^2.2.0 meta: ^1.8.0 - rxdart: ^0.27.0 - stream_chat: ^6.4.0 + rxdart: ^0.27.7 + stream_chat: ^6.5.0 dev_dependencies: build_runner: ^2.3.3 - dart_code_metrics: ^5.7.2 - fake_async: ^1.2.0 + fake_async: ^1.3.1 flutter_test: sdk: flutter - freezed: ^2.3.2 + freezed: ^2.4.0 mocktail: ^0.3.0 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 628f82dc..85d39196 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,11 @@ +## 5.6.0 + +- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19 + +## 5.5.0 + +* Updated `stream_chat_flutter` dependency to [`6.5.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 5.4.0 * Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog). diff --git a/packages/stream_chat_localizations/example/pubspec.yaml b/packages/stream_chat_localizations/example/pubspec.yaml index 5fcac8de..c23b28b1 100644 --- a/packages/stream_chat_localizations/example/pubspec.yaml +++ b/packages/stream_chat_localizations/example/pubspec.yaml @@ -1,29 +1,19 @@ name: stream_chat_localizations_example description: A new Flutter project. -publish_to: 'none' +publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.7.0" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: - path: ../../stream_chat_flutter - stream_chat_localizations: - path: ../ - - -dependency_overrides: - stream_chat_flutter: - path: ../../stream_chat_flutter - -dev_dependencies: - flutter_test: - sdk: flutter + stream_chat_flutter: ^6.4.0 + stream_chat_localizations: ^5.4.0 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index ecd1ae78..a933f492 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,23 +1,22 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 5.4.0 +version: 5.6.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <4.0.0' - flutter: ">=1.20.0" + sdk: ">=2.19.0 <4.0.0" + flutter: ">=3.7.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^6.4.0 + stream_chat_flutter: ^6.6.0 dev_dependencies: - dart_code_metrics: ^5.7.2 flutter_test: sdk: flutter diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 4004994a..c59b0762 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -14,6 +14,7 @@ void main() { expect(localizations.launchUrlError, isNotNull); expect(localizations.loadingUsersError, isNotNull); expect(localizations.noUsersLabel, isNotNull); + expect(localizations.noPhotoOrVideoLabel, isNotNull); expect(localizations.retryLabel, isNotNull); expect(localizations.userLastOnlineText, isNotNull); expect(localizations.userOnlineText, isNotNull); diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 152ed256..245797b7 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.5.0 + +- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19 + ## 6.4.0 - Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index 80a35fc7..f67961aa 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -1,23 +1,18 @@ name: stream_chat_persistence_example description: A new Flutter project. - publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' - flutter: ">=1.17.0" + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.7.0" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^5.1.0 - stream_chat_persistence: ^5.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter + stream_chat: ^6.4.0 + stream_chat_persistence: ^6.4.0 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index b4dfa792..aa4ae55f 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,29 +1,28 @@ 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: 6.4.0 +version: 6.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <4.0.0' - flutter: ">=1.17.0" + sdk: ">=2.19.0 <4.0.0" + flutter: ">=3.7.0" dependencies: - drift: ^2.7.0 + drift: ^2.8.0 flutter: sdk: flutter - logging: ^1.0.1 + logging: ^1.2.0 meta: ^1.8.0 path: ^1.8.2 - path_provider: ^2.0.1 - sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^6.4.0 + path_provider: ^2.0.15 + sqlite3_flutter_libs: ^0.5.15 + stream_chat: ^6.5.0 dev_dependencies: build_runner: ^2.3.3 - dart_code_metrics: ^5.7.2 - drift_dev: ^2.7.0 + drift_dev: ^2.8.3 flutter_test: sdk: flutter mocktail: ^0.3.0 diff --git a/pubspec.yaml b/pubspec.yaml index 9016fa8f..cf418d76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_workspace environment: - sdk: '>=2.18.0 <3.0.0' + sdk: '>=2.19.0 <4.0.0' dev_dependencies: - melos: ^3.0.0 + melos: ^3.1.0