Merge remote-tracking branch 'origin/develop' into refactor/message-status

# Conflicts:
#	packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart
#	packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart
#	packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart
#	packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart
This commit is contained in:
xsahil03x
2023-07-12 17:59:52 +05:30
48 changed files with 1330 additions and 691 deletions
+22 -7
View File
@@ -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 version: 2
updates: updates:
- package-ecosystem: "pub" # See documentation for possible values - package-ecosystem: "github-actions"
directory: "/" # Location of package manifests 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: schedule:
interval: "weekly" interval: "weekly"
-74
View File
@@ -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 }}
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
if: github.ref == 'refs/heads/develop' if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: benc-uk/workflow-dispatch@v1 - uses: benc-uk/workflow-dispatch@v121
with: with:
workflow: build_nightly workflow: build_nightly
repo: GetStream/flutter-samples repo: GetStream/flutter-samples
@@ -23,7 +23,7 @@ jobs:
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: benc-uk/workflow-dispatch@v1 - uses: benc-uk/workflow-dispatch@v121
with: with:
repo: GetStream/flutter-samples repo: GetStream/flutter-samples
workflow: build workflow: build
@@ -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 .
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
conventional_pr_title: conventional_pr_title:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v3.4.0 - uses: amannn/action-semantic-pull-request@v5.2.0
with: with:
scopes: | scopes: |
llc llc
+10 -9
View File
@@ -8,6 +8,7 @@ on:
pull_request: pull_request:
paths: paths:
- 'packages/**' - 'packages/**'
- '.github/workflows/stream_flutter_workflow.yml'
types: types:
- opened - opened
- reopened - reopened
@@ -100,32 +101,32 @@ jobs:
- name: "Collect Coverage" - name: "Collect Coverage"
run: melos run coverage:ignore-file --no-select run: melos run coverage:ignore-file --no-select
- name: "Upload Coverage" - name: "Upload Coverage"
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
with: with:
token: ${{secrets.CODECOV_TOKEN}} token: ${{secrets.CODECOV_TOKEN}}
files: packages/*/coverage/lcov.info files: packages/*/coverage/lcov.info
- name: "Stream Chat Coverage Check" - name: "Stream Chat Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 uses: VeryGoodOpenSource/very_good_coverage@v2.1.0
with: with:
path: packages/stream_chat/coverage/lcov.info path: packages/stream_chat/coverage/lcov.info
min_coverage: 79 min_coverage: 79
- name: "Stream Chat Persistence Coverage Check" - name: "Stream Chat Localizations Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 uses: VeryGoodOpenSource/very_good_coverage@v2.1.0
with: with:
path: packages/stream_chat_localizations/coverage/lcov.info path: packages/stream_chat_localizations/coverage/lcov.info
min_coverage: 88 min_coverage: 100
- name: "Stream Chat Persistence Coverage Check" - name: "Stream Chat Persistence Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 uses: VeryGoodOpenSource/very_good_coverage@v2.1.0
with: with:
path: packages/stream_chat_persistence/coverage/lcov.info path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 95 min_coverage: 95
- name: "Stream Chat Flutter Core Coverage Check" - name: "Stream Chat Flutter Core Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 uses: VeryGoodOpenSource/very_good_coverage@v2.1.0
with: with:
path: packages/stream_chat_flutter_core/coverage/lcov.info path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 30 min_coverage: 30
- name: "Stream Chat Flutter Coverage Check" - name: "Stream Chat Flutter Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 uses: VeryGoodOpenSource/very_good_coverage@v2.1.0
with: with:
path: packages/stream_chat_flutter/coverage/lcov.info path: packages/stream_chat_flutter/coverage/lcov.info
min_coverage: 44 min_coverage: 44
@@ -137,4 +138,4 @@ jobs:
steps: steps:
- name: Run a one-line script - name: Run a one-line script
run: echo Draft PR, you are good. run: echo Draft PR, you are good.
+1
View File
@@ -8,6 +8,7 @@ coverage
.pub/ .pub/
.dart_tool/ .dart_tool/
pubspec.lock pubspec.lock
pubspec_overrides.yaml
flutter_export_environment.sh flutter_export_environment.sh
examples/all_plugins/pubspec.yaml examples/all_plugins/pubspec.yaml
+1 -22
View File
@@ -1,6 +1,4 @@
analyzer: analyzer:
plugins:
- dart_code_metrics
exclude: exclude:
- packages/*/lib/**/*.g.dart - packages/*/lib/**/*.g.dart
- packages/*/lib/scrollable_positioned_list/** - packages/*/lib/scrollable_positioned_list/**
@@ -159,23 +157,4 @@ linter:
- unnecessary_overrides - unnecessary_overrides
- prefer_null_aware_method_calls - prefer_null_aware_method_calls
- use_named_constants - use_named_constants
- use_raw_strings - 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
@@ -40,12 +40,6 @@ Push message delivery behaves according to these rules:
- The message doesn't contain the flag `skip_push` as true. - The message doesn't contain the flag `skip_push` as true.
- `push_notifications` is enabled (default) on the channel type for message is sent. - `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 :::caution
Push notifications require membership. Watching a channel isn't enough. Push notifications require membership. Watching a channel isn't enough.
+6
View File
@@ -1,3 +1,9 @@
## 6.5.0
🔄 Changed
- Updated minimum supported `SDK` version to Dart 2.19
## 6.4.0 ## 6.4.0
🐞 Fixed 🐞 Fixed
+4 -8
View File
@@ -5,18 +5,14 @@ publish_to: "none"
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: dependencies:
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.5
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat: ^6.0.0 stream_chat: ^6.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '6.4.0'; const PACKAGE_VERSION = '6.5.0';
+10 -11
View File
@@ -1,35 +1,34 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: '>=2.17.0 <4.0.0' sdk: '>=2.19.0 <4.0.0'
dependencies: dependencies:
async: ^2.10.0 async: ^2.10.0
collection: ^1.17.0 collection: ^1.17.0
dio: ^5.2.0 dio: ^5.2.1+1
equatable: ^2.0.5 equatable: ^2.0.5
freezed_annotation: ^2.2.0 freezed_annotation: ^2.2.0
http_parser: ^4.0.2 http_parser: ^4.0.2
jose: ^0.3.3 jose: ^0.3.3
json_annotation: ^4.8.0 json_annotation: ^4.8.1
logging: ^1.1.1 logging: ^1.2.0
meta: ^1.8.0 meta: ^1.8.0
mime: ^1.0.4 mime: ^1.0.4
rate_limiter: ^1.0.0 rate_limiter: ^1.0.0
rxdart: ^0.27.7 rxdart: ^0.27.7
synchronized: ^3.0.0 synchronized: ^3.1.0
uuid: ^3.0.7 uuid: ^3.0.7
web_socket_channel: ^2.3.0 web_socket_channel: ^2.4.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
dart_code_metrics: ^5.7.2 freezed: ^2.4.0
freezed: ^2.3.2 json_serializable: ^6.6.2
json_serializable: ^6.6.1
mocktail: ^0.3.0 mocktail: ^0.3.0
test: ^1.24.1 test: ^1.24.3
+79 -16
View File
@@ -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 🐞 Fixed
@@ -6,6 +12,18 @@
after overriding the `onConfirmDeleteTap` callback. after overriding the `onConfirmDeleteTap` callback.
- [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error - [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error
in `SendingIndicatorBuilder`. 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 ✅ Added
@@ -26,6 +44,66 @@
🔄 Changed 🔄 Changed
- Deprecated `StreamMessageInput.attachmentThumbnailBuilders` in favor of `StreamMessageInput.mediaAttachmentBuilder`. - 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 ## 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 ## 6.3.0
🐞 Fixed 🐞 Fixed
@@ -1,6 +1,8 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:responsive_builder/responsive_builder.dart'; import 'package:responsive_builder/responsive_builder.dart';
@@ -277,19 +279,84 @@ class _ChannelPageState extends State<ChannelPage> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: StreamMessageListView( child: StreamMessageListView(
onMessageSwiped:
(CurrentPlatform.isAndroid || CurrentPlatform.isIos)
? reply
: null,
threadBuilder: (context, parent) { threadBuilder: (context, parent) {
return ThreadPage( return ThreadPage(
parent: parent!, parent: parent!,
); );
}, },
messageBuilder: messageBuilder: (
(context, details, messages, defaultWidget) { context,
return defaultWidget.copyWith( messageDetails,
onReplyTap: reply, 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),
); );
}, },
), ),
@@ -1,87 +1,23 @@
name: stream_chat_flutter_example name: stream_chat_flutter_example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none'
# 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
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
collection: ^1.15.0 collection: ^1.15.0
cupertino_icons: ^1.0.4 cupertino_icons: ^1.0.4
flutter: flutter:
sdk: flutter sdk: flutter
responsive_builder: ^0.7.0 responsive_builder: ^0.7.0
stream_chat_flutter: stream_chat_flutter: ^6.4.0
path: ../ stream_chat_localizations: ^5.4.0
stream_chat_localizations: stream_chat_persistence: ^6.4.0
path: ../../stream_chat_localizations
stream_chat_persistence:
path: ../../stream_chat_persistence
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: 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 uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets: assets:
- assets/background_doodle.png - 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
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.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:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; import 'package:stream_chat_flutter/src/attachment/handler/common.dart';
@@ -29,14 +30,16 @@ class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler {
options: options, options: options,
); );
// Open the native file browser so the user can select the download path. // Open the native file browser so the user can select the save location.
final path = await getSavePath(suggestedName: data.fileName); final saveLocation = await getSaveLocation(suggestedName: data.fileName);
if (saveLocation == null) {
if (path == null) {
// Operation was canceled by the user. // Operation was canceled by the user.
return null; return null;
} }
// Get the path to the user's selected location.
final path = saveLocation.path;
// Create an XFile for proper file saving. // Create an XFile for proper file saving.
final file = data.toXFile(path: path); final file = data.toXFile(path: path);
@@ -140,7 +143,6 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
final tempFilePath = tempPath final tempFilePath = tempPath
.resolve(fileName!) .resolve(fileName!)
.toFilePath(windows: CurrentPlatform.isWindows); .toFilePath(windows: CurrentPlatform.isWindows);
print(tempFilePath);
final attachmentFileBytes = attachmentFile.bytes; final attachmentFileBytes = attachmentFile.bytes;
if (attachmentFileBytes == null) { if (attachmentFileBytes == null) {
@@ -189,8 +191,26 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
// Create an XFile for proper file saving. // Create an XFile for proper file saving.
final file = data.toXFile(path: path); 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); 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; return path;
} }
} }
extension on XFile {
/// Deletes this xfile from the file system.
Future<void> delete() async {
final file = File(path);
if (file.existsSync()) {
await file.delete();
}
}
}
@@ -446,8 +446,12 @@ class VideoPackage {
}) : _showControls = showControls, }) : _showControls = showControls,
_autoInitialize = autoInitialize, _autoInitialize = autoInitialize,
_videoPlayerController = _attachment.localUri != null _videoPlayerController = _attachment.localUri != null
? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) ? VideoPlayerController.file(
: VideoPlayerController.network(_attachment.assetUrl!); File.fromUri(_attachment.localUri!),
)
: VideoPlayerController.networkUrl(
Uri.parse(_attachment.assetUrl!),
);
final Attachment _attachment; final Attachment _attachment;
final bool _showControls; final bool _showControls;
@@ -15,7 +15,7 @@ class MessageActionsModal extends StatefulWidget {
required this.message, required this.message,
required this.messageWidget, required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions = true, this.showReactionPicker = true,
this.showDeleteMessage = true, this.showDeleteMessage = true,
this.showEditMessage = true, this.showEditMessage = true,
this.onReplyTap, this.onReplyTap,
@@ -54,8 +54,8 @@ class MessageActionsModal extends StatefulWidget {
/// [StreamMessageThemeData] for message /// [StreamMessageThemeData] for message
final StreamMessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Flag for showing reactions /// Flag for showing reaction picker.
final bool showReactions; final bool showReactionPicker;
/// Callback when copy is tapped /// Callback when copy is tapped
final OnMessageTap? onCopyTap; final OnMessageTap? onCopyTap;
@@ -103,6 +103,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final user = StreamChat.of(context).currentUser; final user = StreamChat.of(context).currentUser;
final orientation = mediaQueryData.orientation; final orientation = mediaQueryData.orientation;
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final fontSize = widget.messageTheme.messageTextStyle?.fontSize; final fontSize = widget.messageTheme.messageTextStyle?.fontSize;
final streamChatThemeData = StreamChatTheme.of(context); final streamChatThemeData = StreamChatTheme.of(context);
@@ -113,11 +117,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Column( child: Column(
crossAxisAlignment: widget.reverse mainAxisAlignment: MainAxisAlignment.center,
? CrossAxisAlignment.end crossAxisAlignment: CrossAxisAlignment.stretch,
: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (widget.showReactions && widget.message.state.isCompleted) if (widget.showReactionPicker && hasReactionPermission)
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Align( return Align(
@@ -382,8 +382,9 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = VideoPlayerController.network(widget.attachment.assetUrl!) _controller = VideoPlayerController.networkUrl(
..initialize().then((_) { Uri.parse(widget.attachment.assetUrl!),
)..initialize().then((_) {
// ignore: no-empty-block // ignore: no-empty-block
setState(() {}); //when your thumbnail will show. setState(() {}); //when your thumbnail will show.
}); });
@@ -147,7 +147,6 @@ class StreamMessageInput extends StatefulWidget {
_defaultClearQuotedMessageKeyPredicate, _defaultClearQuotedMessageKeyPredicate,
this.ogPreviewFilter = _defaultOgPreviewFilter, this.ogPreviewFilter = _defaultOgPreviewFilter,
this.hintGetter = _defaultHintGetter, this.hintGetter = _defaultHintGetter,
this.contentInsertionConfiguration,
}); });
/// The predicate used to send a message on desktop/web /// 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. /// Returns the hint text for the message input.
final HintGetter hintGetter; final HintGetter hintGetter;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
static String? _defaultHintGetter( static String? _defaultHintGetter(
BuildContext context, BuildContext context,
HintType type, HintType type,
@@ -908,8 +904,6 @@ class StreamMessageInputState extends State<StreamMessageInput>
decoration: _getInputDecoration(context), decoration: _getInputDecoration(context),
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
autocorrect: widget.autoCorrect, autocorrect: widget.autoCorrect,
contentInsertionConfiguration:
widget.contentInsertionConfiguration,
), ),
), ),
), ),
@@ -208,7 +208,7 @@ class MessageInputFileAttachments extends StatelessWidget {
message: Message(), // dummy message message: Message(), // dummy message
attachment: attachment, attachment: attachment,
constraints: BoxConstraints.loose(Size( constraints: BoxConstraints.loose(Size(
MediaQuery.sizeOf(context).width * 0.65, MediaQuery.of(context).size.width * 0.65,
56, 56,
)), )),
trailing: Padding( trailing: Padding(
@@ -120,7 +120,6 @@ class StreamMessageTextField extends StatefulWidget {
this.restorationId, this.restorationId,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.contentInsertionConfiguration,
}) : assert(obscuringCharacter.length == 1, ''), }) : assert(obscuringCharacter.length == 1, ''),
smartDashesType = smartDashesType ?? smartDashesType = smartDashesType ??
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
@@ -527,9 +526,6 @@ class StreamMessageTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning; final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
@override @override
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); _StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
@@ -626,9 +622,6 @@ class StreamMessageTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>( properties.add(DiagnosticsProperty<bool>(
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
defaultValue: true)); defaultValue: true));
properties.add(DiagnosticsProperty<ContentInsertionConfiguration>(
'contentInsertionConfiguration', contentInsertionConfiguration,
defaultValue: null));
} }
} }
@@ -734,7 +727,6 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
restorationId: widget.restorationId, restorationId: widget.restorationId,
scribbleEnabled: widget.scribbleEnabled, scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
contentInsertionConfiguration: widget.contentInsertionConfiguration,
); );
@override @override
@@ -1,5 +1,7 @@
// ignore_for_file: lines_longer_than_80_chars // ignore_for_file: lines_longer_than_80_chars
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.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/mlv_utils.dart';
import 'package:stream_chat_flutter/src/message_list_view/thread_separator.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/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'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Spacing Types (These are properties of a message to help inform the decision /// Spacing Types (These are properties of a message to help inform the decision
@@ -96,6 +97,10 @@ class StreamMessageListView extends StatefulWidget {
this.initialAlignment, this.initialAlignment,
this.scrollController, this.scrollController,
this.itemPositionListener, 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.onMessageSwiped,
this.highlightInitialMessage = false, this.highlightInitialMessage = false,
this.messageHighlightColor, this.messageHighlightColor,
@@ -1226,26 +1231,6 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
} }
var child = messageWidget; 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 && if (!initialMessageHighlightComplete &&
widget.highlightInitialMessage && widget.highlightInitialMessage &&
isInitialMessage(message.id, streamChannel)) { isInitialMessage(message.id, streamChannel)) {
@@ -1269,6 +1254,77 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
), ),
); );
} }
// 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; return child;
} }
@@ -188,19 +188,16 @@ class _MessageCardState extends State<MessageCard> {
attachmentPadding: widget.attachmentPadding, attachmentPadding: widget.attachmentPadding,
), ),
if (!widget.isGiphy) if (!widget.isGiphy)
ConstrainedBox( TextBubble(
constraints: BoxConstraints.loose(const Size.fromWidth(500)), messageTheme: widget.messageTheme,
child: TextBubble( message: widget.message,
messageTheme: widget.messageTheme, textPadding: widget.textPadding,
message: widget.message, textBuilder: widget.textBuilder,
textPadding: widget.textPadding, isOnlyEmoji: widget.isOnlyEmoji,
textBuilder: widget.textBuilder, hasQuotedMessage: widget.hasQuotedMessage,
isOnlyEmoji: widget.isOnlyEmoji, hasUrlAttachments: widget.hasUrlAttachments,
hasQuotedMessage: widget.hasQuotedMessage, onLinkTap: widget.onLinkTap,
hasUrlAttachments: widget.hasUrlAttachments, onMentionTap: widget.onMentionTap,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
),
), ),
if (widget.hasUrlAttachments && !widget.hasQuotedMessage) if (widget.hasUrlAttachments && !widget.hasQuotedMessage)
_buildUrlAttachment(), _buildUrlAttachment(),
@@ -2,6 +2,7 @@ import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart' hide ButtonStyle; import 'package:flutter/material.dart' hide ButtonStyle;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_portal/flutter_portal.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/conditional_parent_builder/conditional_parent_builder.dart';
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_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'; 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.attachmentBorderRadiusGeometry,
this.onMentionTap, this.onMentionTap,
this.onMessageTap, this.onMessageTap,
this.showReactionPickerIndicator = false, bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead')
bool showReactionPickerIndicator = true,
@internal this.showReactionPickerTail = false,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.showSendingIndicator = true, this.showSendingIndicator = true,
this.showThreadReplyIndicator = false, this.showThreadReplyIndicator = false,
@@ -114,6 +118,7 @@ class StreamMessageWidget extends StatefulWidget {
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null, bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders', 'You can only use one of the two bottom row builders',
), ),
showReactionPicker = showReactionPicker ?? showReactionPickerIndicator,
attachmentBuilders = { attachmentBuilders = {
'image': (context, message, attachments) { 'image': (context, message, attachments) {
final border = RoundedRectangleBorder( final border = RoundedRectangleBorder(
@@ -460,10 +465,22 @@ class StreamMessageWidget extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final void Function(String)? onLinkTap; 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} /// {@template showReactionPickerIndicator}
/// Used in [StreamMessageReactionsModal] and [MessageActionsModal] /// 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} /// {@endtemplate}
final bool showReactionPickerIndicator; @internal
final bool showReactionPickerTail;
/// {@template onShowMessage} /// {@template onShowMessage}
/// Callback when show message is tapped /// Callback when show message is tapped
@@ -619,7 +636,10 @@ class StreamMessageWidget extends StatefulWidget {
bool? showInChannelIndicator, bool? showInChannelIndicator,
void Function(User)? onUserAvatarTap, void Function(User)? onUserAvatarTap,
void Function(String)? onLinkTap, void Function(String)? onLinkTap,
bool? showReactionPicker,
@Deprecated('Use `showReactionPicker` instead')
bool? showReactionPickerIndicator, bool? showReactionPickerIndicator,
@internal bool? showReactionPickerTail,
List<Read>? readList, List<Read>? readList,
ShowMessageCallback? onShowMessage, ShowMessageCallback? onShowMessage,
bool? showUsername, bool? showUsername,
@@ -703,8 +723,11 @@ class StreamMessageWidget extends StatefulWidget {
showInChannelIndicator ?? this.showInChannelIndicator, showInChannelIndicator ?? this.showInChannelIndicator,
onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap,
onLinkTap: onLinkTap ?? this.onLinkTap, onLinkTap: onLinkTap ?? this.onLinkTap,
showReactionPickerIndicator: showReactionPicker: showReactionPicker ??
showReactionPickerIndicator ?? this.showReactionPickerIndicator, showReactionPickerIndicator ??
this.showReactionPicker,
showReactionPickerTail:
showReactionPickerTail ?? this.showReactionPickerTail,
onShowMessage: onShowMessage ?? this.onShowMessage, onShowMessage: onShowMessage ?? this.onShowMessage,
showUsername: showUsername ?? this.showUsername, showUsername: showUsername ?? this.showUsername,
showTimestamp: showTimestamp ?? this.showTimestamp, showTimestamp: showTimestamp ?? this.showTimestamp,
@@ -883,12 +906,12 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
if (!widget.message.state.isDeleted) { if (!widget.message.state.isDeleted) {
return ContextMenuArea( return ContextMenuArea(
verticalPadding: 0, verticalPadding: 0,
builder: (context) => _buildContextMenu(), builder: (_) => _buildContextMenu(),
child: child, child: child,
); );
} else {
return child;
} }
return child;
}, },
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
@@ -961,9 +984,9 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
messageWidget: widget, messageWidget: widget,
showBottomRow: showBottomRow, showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight, showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator: showReactionPickerTail: widget.showReactionPickerTail,
widget.showReactionPickerIndicator,
showReactions: showReactions, showReactions: showReactions,
onReactionsTap: () => _showMessageReactionsModal(context),
showUserAvatar: widget.showUserAvatar, showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat, streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar, translateUserAvatar: widget.translateUserAvatar,
@@ -994,14 +1017,15 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
return [ return [
StreamChatContextMenuItem( if (widget.showReactionPicker)
child: StreamChannel( StreamChatContextMenuItem(
channel: channel, child: StreamChannel(
child: ContextMenuReactionPicker( channel: channel,
message: widget.message, child: ContextMenuReactionPicker(
message: widget.message,
),
), ),
), ),
),
if (shouldShowReplyAction) ...[ if (shouldShowReplyAction) ...[
StreamChatContextMenuItem( StreamChatContextMenuItem(
leading: StreamSvgIcon.reply(), leading: StreamSvgIcon.reply(),
@@ -1145,20 +1169,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
]; ];
} }
void onLongPress(BuildContext context) { void _showMessageReactionsModal(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) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showDialog( showDialog(
@@ -1168,7 +1179,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: StreamMessageReactionsModal(
showReactionPicker: widget.showReactionPicker,
messageWidget: widget.copyWith( messageWidget: widget.copyWith(
key: const Key('MessageWidget'), key: const Key('MessageWidget'),
message: widget.message.copyWith( message: widget.message.copyWith(
@@ -1182,37 +1194,92 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: false, showSendingIndicator: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
showReactionPickerIndicator: // Show the tail if the reaction picker is visible.
widget.showReactions && widget.message.state.isCompleted, showReactionPickerTail: widget.showReactionPicker,
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id widget.message.user!.id == channel.client.state.currentUser!.id
? DisplayWidget.gone ? DisplayWidget.gone
: DisplayWidget.show, : DisplayWidget.show,
), ),
onCopyTap: (message) { onUserAvatarTap: widget.onUserAvatarTap,
final text = message.text;
if (text != null) Clipboard.setData(ClipboardData(text: text));
},
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: shouldShowDeleteAction,
onConfirmDeleteTap: widget.onConfirmDeleteTap,
message: widget.message, 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,
),
);
},
);
}
} }
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.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/message_widget_content_components.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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. /// Should not be used outside of [MessageWidget.
/// {@endtemplate} /// {@endtemplate}
@internal
class MessageWidgetContent extends StatelessWidget { class MessageWidgetContent extends StatelessWidget {
/// {@macro messageWidgetContent} /// {@macro messageWidgetContent}
const MessageWidgetContent({ const MessageWidgetContent({
@@ -33,6 +35,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.showUserAvatar, required this.showUserAvatar,
required this.avatarWidth, required this.avatarWidth,
required this.showReactions, required this.showReactions,
required this.onReactionsTap,
required this.messageTheme, required this.messageTheme,
required this.shouldShowReactions, required this.shouldShowReactions,
required this.streamChatTheme, required this.streamChatTheme,
@@ -45,7 +48,7 @@ class MessageWidgetContent extends StatelessWidget {
required this.attachmentBuilders, required this.attachmentBuilders,
required this.attachmentPadding, required this.attachmentPadding,
required this.textPadding, required this.textPadding,
required this.showReactionPickerIndicator, required this.showReactionPickerTail,
required this.translateUserAvatar, required this.translateUserAvatar,
required this.bottomRowPadding, required this.bottomRowPadding,
required this.showInChannel, required this.showInChannel,
@@ -111,6 +114,11 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro showReactions} /// {@macro showReactions}
final bool 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} /// {@macro messageTheme}
final StreamMessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
@@ -174,8 +182,8 @@ class MessageWidgetContent extends StatelessWidget {
/// {@macro quotedMessageBuilder} /// {@macro quotedMessageBuilder}
final Widget Function(BuildContext, Message)? quotedMessageBuilder; final Widget Function(BuildContext, Message)? quotedMessageBuilder;
/// {@macro showReactionPickerIndicator} /// {@macro showReactionPickerTail}
final bool showReactionPickerIndicator; final bool showReactionPickerTail;
/// {@macro translateUserAvatar} /// {@macro translateUserAvatar}
final bool translateUserAvatar; final bool translateUserAvatar;
@@ -288,9 +296,7 @@ class MessageWidgetContent extends StatelessWidget {
ownId: streamChat.currentUser!.id, ownId: streamChat.currentUser!.id,
reverse: reverse, reverse: reverse,
shouldShowReactions: shouldShowReactions, shouldShowReactions: shouldShowReactions,
onTap: () => _showMessageReactionsModal( onTap: onReactionsTap,
context,
),
) )
: null, : null,
anchor: Aligned( anchor: Aligned(
@@ -363,7 +369,8 @@ class MessageWidgetContent extends StatelessWidget {
shape: shape, shape: shape,
), ),
), ),
if (showReactionPickerIndicator) // TODO: Make tail part of the Reaction Picker.
if (showReactionPickerTail)
Positioned( Positioned(
right: reverse ? null : 4, right: reverse ? null : 4,
left: reverse ? 4 : null, left: reverse ? 4 : null,
@@ -374,6 +381,7 @@ class MessageWidgetContent extends StatelessWidget {
Colors.transparent, Colors.transparent,
Colors.transparent, Colors.transparent,
tailCirclesSpace: 1, 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) { Widget _buildBottomRow(BuildContext context) {
final defaultWidget = BottomRow( final defaultWidget = BottomRow(
message: message, message: message,
@@ -15,7 +15,7 @@ class StreamMessageReactionsModal extends StatelessWidget {
required this.message, required this.message,
required this.messageWidget, required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions, this.showReactionPicker = true,
this.reverse = false, this.reverse = false,
this.onUserAvatarTap, this.onUserAvatarTap,
}); });
@@ -32,8 +32,8 @@ class StreamMessageReactionsModal extends StatelessWidget {
/// {@macro reverse} /// {@macro reverse}
final bool reverse; final bool reverse;
/// {@macro showReactions} /// Flag for showing reaction picker.
final bool? showReactions; final bool showReactionPicker;
/// {@macro onUserAvatarTap} /// {@macro onUserAvatarTap}
final void Function(User)? onUserAvatarTap; final void Function(User)? onUserAvatarTap;
@@ -56,8 +56,7 @@ class StreamMessageReactionsModal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
if ((showReactions ?? hasReactionPermission) && if (showReactionPicker && hasReactionPermission)
message.state.isCompleted)
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Align( return Align(
@@ -43,19 +43,9 @@ double calculateReactionsHorizontalAlignment(
final signal = user?.id == message.user?.id ? 1 : -1; final signal = user?.id == message.user?.id ? 1 : -1;
final result = signal * (1 - divFactor * 2.0); 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
// Ensure reactions don't get pushed past the edge of the screen. // move the model all the way to the end of screen.
// return result.clamp(-1, 1);
// 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;
}
} }
@@ -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;
}
}
@@ -1,173 +1,464 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template swipeable} /// Signature used by [swipeable] to indicate that it has been swiped in
/// A swipeable tile in a list. Swiping on the tile will reveal actions that /// the given `direction`.
/// can be taken. ///
/// {@endtemplate} /// Used by [Swipeable.onSwiped].
class Swipeable extends StatefulWidget { typedef SwipeDirectionCallback = void Function(SwipeDirection direction);
/// {@macro swipeable}
const Swipeable({
super.key,
required this.child,
required this.backgroundIcon,
this.onSwipeStart,
this.onSwipeCancel,
this.onSwipeEnd,
this.threshold = 82.0,
});
/// Child to make swipeable /// Signature for a function that builds a widget given the progress of the
final Widget child; /// dismissing action.
///
/// Used by [Swipeable.backgroundBuilder].
typedef BackgroundWidgetBuilder = Widget Function(
BuildContext context,
SwipeUpdateDetails details,
);
/// Background icon after swipe /// The direction in which a [Swipeable] can be swiped.
final Widget backgroundIcon; enum SwipeDirection {
/// The [Swipeable] can be swiped by dragging either left or right.
horizontal,
/// The action to perform when the swipe starts /// The [Swipeable] can be swiped by dragging in the reverse of the
final VoidCallback? onSwipeStart; /// reading direction (e.g., from right to left in left-to-right languages).
endToStart,
/// The action to perform when the swipe is cancelled /// The [Swipeable] can be swiped by dragging in the reading direction
final VoidCallback? onSwipeCancel; /// (e.g., from left to right in left-to-right languages).
startToEnd,
/// The action to perform when the swipe ends /// The [Swipeable] cannot be swiped by dragging.
final VoidCallback? onSwipeEnd; none
/// Threshold for swipe
final double threshold;
@override
State<StatefulWidget> createState() => _SwipeableState();
} }
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin { /// A widget that can be swiped in a specified direction.
double _dragExtent = 0; ///
late AnimationController _moveController; /// The `Swipeable` widget allows its child to be swiped by the user in a
late AnimationController _iconMoveController; /// specified direction.
late Animation<Offset> _moveAnimation; ///
late Animation<Offset> _iconTransitionAnimation; /// It provides options for customizing the swipe behavior, including the
late Animation<double> _iconFadeAnimation; /// ability to specify a background widget that appears during the swipe,
bool _pastThreshold = false; /// 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<Rect> {
_SwipeableClipper({
required this.moveAnimation,
}) : super(reclip: moveAnimation);
final Animation<Offset> 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<Swipeable>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_moveController = _moveController = AnimationController(
AnimationController(duration: _animationDuration, vsync: this); duration: widget.movementDuration,
_iconMoveController = vsync: this,
AnimationController(duration: _animationDuration, vsync: this); )..addStatusListener((_) => updateKeepAlive());
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0)) _updateMoveAnimation();
.animate(_moveController);
_iconTransitionAnimation =
Tween<Offset>(begin: const Offset(-0.1, 0), end: const Offset(0.4, 0))
.animate(_moveController);
_iconFadeAnimation =
Tween<double>(begin: 0.7, end: 1).animate(_iconMoveController);
const controllerValue = 0.0;
_moveController.animateTo(controllerValue);
_iconMoveController.animateTo(controllerValue);
} }
AnimationController? _moveController;
late Animation<Offset> _moveAnimation;
double _dragExtent = 0;
bool _dragUnderway = false;
bool _swipeThresholdReached = false;
final GlobalKey _contentKey = GlobalKey();
@override
bool get wantKeepAlive => _moveController?.isAnimating ?? false;
@override @override
void dispose() { void dispose() {
_moveController.dispose(); _moveController?.dispose();
_iconMoveController.dispose();
super.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) { 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) { void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _moveController!.isAnimating) {
return;
}
final delta = details.primaryDelta!; final delta = details.primaryDelta!;
_dragExtent += delta; final oldDragExtent = _dragExtent;
switch (widget.direction) {
if (_dragExtent.isNegative) return; case SwipeDirection.horizontal:
_dragExtent += delta;
final movePastThresholdPixels = widget.threshold; break;
var newPos = _dragExtent.abs() / context.size!.width; case SwipeDirection.endToStart:
switch (Directionality.of(context)) {
if (_dragExtent.abs() > movePastThresholdPixels) { case TextDirection.rtl:
// how many "thresholds" past the threshold we are. 1 = the threshold 2 if (_dragExtent + delta > 0) {
// = two thresholds. _dragExtent += delta;
final n = _dragExtent.abs() / movePastThresholdPixels; }
break;
// Take the number of thresholds past the threshold, and reduce this case TextDirection.ltr:
// number if (_dragExtent + delta < 0) {
final reducedThreshold = math.pow(n, 0.3); _dragExtent += delta;
}
final adjustedPixelPos = movePastThresholdPixels * reducedThreshold; break;
newPos = adjustedPixelPos / context.size!.width; }
break;
if (_dragExtent > 0 && !_pastThreshold) { case SwipeDirection.startToEnd:
_iconMoveController.value = 1; switch (Directionality.of(context)) {
_pastThreshold = true; case TextDirection.rtl:
} if (_dragExtent + delta < 0) {
} else { _dragExtent += delta;
// Send a cancel event if the user has swiped back underneath the }
// threshold break;
if (_pastThreshold && widget.onSwipeCancel != null) { case TextDirection.ltr:
widget.onSwipeCancel!(); if (_dragExtent + delta > 0) {
} _dragExtent += delta;
_pastThreshold = false; }
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<Offset>(
begin: Offset.zero,
end: Offset(end, 0),
),
);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
_moveController.animateTo(0, duration: _animationDuration); if (!_isActive || _moveController!.isAnimating) return;
_iconMoveController.animateTo(0, duration: _animationDuration); _dragUnderway = false;
_dragExtent = 0.0;
if (_pastThreshold && widget.onSwipeEnd != null) { // Once dragging ends, animate back to the initial offset.
widget.onSwipeEnd!(); _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 @override
Widget build(BuildContext context) { 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: <Widget>[
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( return GestureDetector(
onHorizontalDragStart: _handleDragStart, onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd, onHorizontalDragEnd: _handleDragEnd,
behavior: HitTestBehavior.opaque, behavior: widget.behavior,
child: Stack( dragStartBehavior: widget.dragStartBehavior,
alignment: Alignment.center, child: content,
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,
),
],
),
); );
} }
} }
@@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget's appearance. /// Modify it to change the widget's appearance.
/// {@endtemplate} /// {@endtemplate}
@Deprecated('Use `StreamUserListTile` instead.')
class StreamUserItem extends StatelessWidget { class StreamUserItem extends StatelessWidget {
/// {@macro streamUserItem} /// {@macro streamUserItem}
const StreamUserItem({ const StreamUserItem({
@@ -88,7 +88,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?);
typedef ChannelInfoCallback = void Function(Channel); typedef ChannelInfoCallback = void Function(Channel);
/// {@template channelPreviewBuilder} /// {@template channelPreviewBuilder}
/// Builder used to create a custom [ChannelPreview] for a [Channel] /// Builder used to create a custom ChannelPreview for a [Channel]
/// {@endtemplate} /// {@endtemplate}
@Deprecated('Use StreamChannelListViewIndexedWidgetBuilder instead') @Deprecated('Use StreamChannelListViewIndexedWidgetBuilder instead')
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
@@ -58,6 +58,7 @@ export 'src/message_widget/message_text.dart';
export 'src/message_widget/message_widget.dart'; export 'src/message_widget/message_widget.dart';
export 'src/message_widget/reactions/reaction_picker.dart'; export 'src/message_widget/reactions/reaction_picker.dart';
export 'src/message_widget/text_bubble.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/back_button.dart';
export 'src/misc/connection_status_builder.dart'; export 'src/misc/connection_status_builder.dart';
export 'src/misc/date_divider.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/reaction_icon.dart';
export 'src/misc/stream_neumorphic_button.dart'; export 'src/misc/stream_neumorphic_button.dart';
export 'src/misc/stream_svg_icon.dart'; export 'src/misc/stream_svg_icon.dart';
export 'src/misc/swipeable.dart';
export 'src/misc/system_message.dart'; export 'src/misc/system_message.dart';
export 'src/misc/thread_header.dart'; export 'src/misc/thread_header.dart';
export 'src/misc/visible_footnote.dart'; export 'src/misc/visible_footnote.dart';
+27 -28
View File
@@ -1,50 +1,50 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/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. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: ">=2.17.0 <4.0.0" sdk: ">=2.19.0 <4.0.0"
flutter: ">=1.20.0" flutter: ">=3.7.0"
dependencies: dependencies:
cached_network_image: ^3.0.0 cached_network_image: ^3.2.3
chewie: ^1.3.4 chewie: ^1.7.0
collection: ^1.17.0 collection: ^1.17.0
contextmenu: ^3.0.0 contextmenu: ^3.0.0
dart_vlc: ^0.4.0 dart_vlc: ^0.4.0
desktop_drop: ^0.4.0 desktop_drop: ^0.4.1
diacritic: ^0.1.3 diacritic: ^0.1.4
dio: ^5.2.0 dio: ^5.2.1+1
ezanimation: ^0.6.0 ezanimation: ^0.6.0
file_picker: ^5.2.4 file_picker: ^5.3.1
file_selector: ^0.9.0 file_selector: ^1.0.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_markdown: ^0.6.1 flutter_markdown: ^0.6.15
flutter_portal: ^1.0.0 flutter_portal: ^1.1.4
flutter_svg: ^2.0.4 flutter_svg: ^2.0.5
http_parser: ^4.0.0 http_parser: ^4.0.2
image_gallery_saver: ^2.0.1 image_gallery_saver: ^2.0.3
image_picker: ^0.8.2 image_picker: ^1.0.0
jiffy: ^5.0.0 jiffy: ^5.0.0
lottie: ^2.0.0 lottie: ^2.3.2
meta: ^1.8.0 meta: ^1.8.0
path_provider: ^2.0.9 path_provider: ^2.0.15
photo_manager: ^2.5.2 photo_manager: ^2.6.0
photo_view: ^0.14.0 photo_view: ^0.14.0
rxdart: ^0.27.0 rxdart: ^0.27.7
share_plus: ^6.3.0 share_plus: ^7.0.2
shimmer: ^3.0.0 shimmer: ^3.0.0
stream_chat_flutter_core: ^6.4.0 stream_chat_flutter_core: ^6.5.0
synchronized: ^3.0.0 synchronized: ^3.1.0
thumblr: ^0.0.4 thumblr: ^0.0.4
url_launcher: ^6.1.0 url_launcher: ^6.1.11
video_player: ^2.4.5 video_player: ^2.7.0
video_player_macos: ^2.0.1 video_player_macos: ^2.0.1
video_thumbnail: ^0.5.0 video_thumbnail: ^0.5.3
flutter: flutter:
plugin: plugin:
@@ -70,9 +70,8 @@ flutter:
uses-material-design: true uses-material-design: true
dev_dependencies: dev_dependencies:
dart_code_metrics: ^5.7.2
flutter_test: flutter_test:
sdk: flutter sdk: flutter
golden_toolkit: ^0.15.0 golden_toolkit: ^0.15.0
mocktail: ^0.3.0 mocktail: ^0.3.0
path: ^1.8.2 path: ^1.8.2
@@ -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 = <Attachment>[];
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 = <Attachment>[];
await tester.pumpWidget(
wrapWithStreamChat(
MessageInputMediaAttachments(
attachments: attachments,
),
),
);
// Expect an empty box
expect(find.byType(SizedBox), findsOneWidget);
},
);
});
}
@@ -104,7 +104,7 @@ void main() {
message: message, message: message,
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
reverse: true, reverse: true,
showReactions: false, showReactionPicker: false,
onUserAvatarTap: onUserAvatarTap, onUserAvatarTap: onUserAvatarTap,
), ),
), ),
@@ -1,3 +1,7 @@
## 6.5.0
- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19
## 6.4.0 ## 6.4.0
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -1,76 +1,17 @@
name: stream_chat_flutter_core_example name: stream_chat_flutter_core_example
description: Example app for testing stream_chat_flutter_core description: Example app for testing stream_chat_flutter_core
publish_to: 'none'
# 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
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: 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 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: 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: 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 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
+9 -10
View File
@@ -1,30 +1,29 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: '>=2.17.0 <4.0.0' sdk: ">=2.19.0 <4.0.0"
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: dependencies:
collection: ^1.17.0 collection: ^1.17.0
connectivity_plus: ^4.0.0 connectivity_plus: ^4.0.1
flutter: flutter:
sdk: flutter sdk: flutter
freezed_annotation: ^2.0.3 freezed_annotation: ^2.2.0
meta: ^1.8.0 meta: ^1.8.0
rxdart: ^0.27.0 rxdart: ^0.27.7
stream_chat: ^6.4.0 stream_chat: ^6.5.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
dart_code_metrics: ^5.7.2 fake_async: ^1.3.1
fake_async: ^1.2.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
freezed: ^2.3.2 freezed: ^2.4.0
mocktail: ^0.3.0 mocktail: ^0.3.0
@@ -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 ## 5.4.0
* Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog). * Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
@@ -1,29 +1,19 @@
name: stream_chat_localizations_example name: stream_chat_localizations_example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
flutter: ">=3.7.0"
dependencies: dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter: stream_chat_flutter: ^6.4.0
path: ../../stream_chat_flutter stream_chat_localizations: ^5.4.0
stream_chat_localizations:
path: ../
dependency_overrides:
stream_chat_flutter:
path: ../../stream_chat_flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true
@@ -1,23 +1,22 @@
name: stream_chat_localizations name: stream_chat_localizations
description: The Official localizations for Stream Chat Flutter, a service for building chat applications 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 homepage: https://github.com/GetStream/stream-chat-flutter
repository: 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 issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: '>=2.17.0 <4.0.0' sdk: ">=2.19.0 <4.0.0"
flutter: ">=1.20.0" flutter: ">=3.7.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
stream_chat_flutter: ^6.4.0 stream_chat_flutter: ^6.6.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^5.7.2
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -14,6 +14,7 @@ void main() {
expect(localizations.launchUrlError, isNotNull); expect(localizations.launchUrlError, isNotNull);
expect(localizations.loadingUsersError, isNotNull); expect(localizations.loadingUsersError, isNotNull);
expect(localizations.noUsersLabel, isNotNull); expect(localizations.noUsersLabel, isNotNull);
expect(localizations.noPhotoOrVideoLabel, isNotNull);
expect(localizations.retryLabel, isNotNull); expect(localizations.retryLabel, isNotNull);
expect(localizations.userLastOnlineText, isNotNull); expect(localizations.userLastOnlineText, isNotNull);
expect(localizations.userOnlineText, isNotNull); expect(localizations.userOnlineText, isNotNull);
@@ -1,3 +1,7 @@
## 6.5.0
- Updated minimum supported `SDK` version to Flutter 3.7/Dart 2.19
## 6.4.0 ## 6.4.0
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog). - Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -1,23 +1,18 @@
name: stream_chat_persistence_example name: stream_chat_persistence_example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat: ^5.1.0 stream_chat: ^6.4.0
stream_chat_persistence: ^5.1.0 stream_chat_persistence: ^6.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true
+9 -10
View File
@@ -1,29 +1,28 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: '>=2.17.0 <4.0.0' sdk: ">=2.19.0 <4.0.0"
flutter: ">=1.17.0" flutter: ">=3.7.0"
dependencies: dependencies:
drift: ^2.7.0 drift: ^2.8.0
flutter: flutter:
sdk: flutter sdk: flutter
logging: ^1.0.1 logging: ^1.2.0
meta: ^1.8.0 meta: ^1.8.0
path: ^1.8.2 path: ^1.8.2
path_provider: ^2.0.1 path_provider: ^2.0.15
sqlite3_flutter_libs: ^0.5.0 sqlite3_flutter_libs: ^0.5.15
stream_chat: ^6.4.0 stream_chat: ^6.5.0
dev_dependencies: dev_dependencies:
build_runner: ^2.3.3 build_runner: ^2.3.3
dart_code_metrics: ^5.7.2 drift_dev: ^2.8.3
drift_dev: ^2.7.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.3.0 mocktail: ^0.3.0
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter_workspace name: stream_chat_flutter_workspace
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.19.0 <4.0.0'
dev_dependencies: dev_dependencies:
melos: ^3.0.0 melos: ^3.1.0