Merge pull request #1659 from GetStream/release/6.5.0
This commit is contained in:
+22
-7
@@ -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"
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
-6
@@ -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.
|
||||||
|
|||||||
@@ -5,14 +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.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
dart_code_metrics: ^5.7.2
|
|
||||||
freezed: ^2.3.2
|
freezed: ^2.3.2
|
||||||
json_serializable: ^6.6.1
|
json_serializable: ^6.6.1
|
||||||
mocktail: ^0.3.0
|
mocktail: ^0.3.0
|
||||||
|
|||||||
@@ -1,3 +1,105 @@
|
|||||||
|
## 6.5.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1620]](https://github.com/GetStream/stream-chat-flutter/issues/1620) Fixed messages Are Not Hard Deleting even
|
||||||
|
after overriding the `onConfirmDeleteTap` callback.
|
||||||
|
- [[#1621]](https://github.com/GetStream/stream-chat-flutter/issues/1621) Fixed `createdAtStyle` null check error
|
||||||
|
in `SendingIndicatorBuilder`.
|
||||||
|
- [[#1069]](https://github.com/GetStream/stream-chat-flutter/issues/1069) Fixed message swipe to reply using same
|
||||||
|
direction for both current user and other users. It now uses `SwipeDirection.startToEnd` for current user
|
||||||
|
and `SwipeDirection.endToStart` for other users.
|
||||||
|
- [[#1590]](https://github.com/GetStream/stream-chat-flutter/issues/1590)
|
||||||
|
Fixed `StreamMessageWidget.showReactionPickerIndicator` not toggling the reaction picker indicator visibility.
|
||||||
|
- [[#1639]](https://github.com/GetStream/stream-chat-flutter/issues/1639) Fixed attachments not showing in gallery view
|
||||||
|
even after saving them to the device.
|
||||||
|
> **Note**
|
||||||
|
> This fix depends on the [image_gallery_saver](https://pub.dev/packages/image_gallery_saver) plugin. Make sure to add
|
||||||
|
necessary permissions in your App as per the plugin documentation.
|
||||||
|
- [[#1642]](https://github.com/GetStream/stream-chat-flutter/issues/1642) Fixed `StreamMessageWidget.widthFactor` not
|
||||||
|
working on web and desktop platforms.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added support for customizing attachments in `StreamMessageInput`. Use various properties mentioned
|
||||||
|
below. [#1511](https://github.com/GetStream/stream-chat-flutter/issues/1511)
|
||||||
|
* `StreamMessageInput.attachmentListBuilder` to customize the attachment list.
|
||||||
|
* `StreamMessageInput.fileAttachmentListBuilder` to customize the file attachment list.
|
||||||
|
* `StreamMessageInput.mediaAttachmentListBuilder` to customize the media attachment list. Includes images, videos
|
||||||
|
and gifs.
|
||||||
|
* `StreamMessageInput.fileAttachmentBuilder` to customize the file attachment item shown in `FileAttachmentList`.
|
||||||
|
* `StreamMessageInput.mediaAttachmentBuilder` to customize the media attachment item shown in
|
||||||
|
`MediaAttachmentList`.
|
||||||
|
|
||||||
|
|
||||||
|
- Added `StreamMessageInput.quotedMessageAttachmentThumbnailBuilders` to customize the thumbnail builders for quoted
|
||||||
|
message attachments.
|
||||||
|
|
||||||
|
🔄 Changed
|
||||||
|
|
||||||
|
- Deprecated `StreamMessageInput.attachmentThumbnailBuilders` in favor of `StreamMessageInput.mediaAttachmentBuilder`.
|
||||||
|
- Deprecated `StreamMessageListView.onMessageSwiped`. Try wrapping the `MessageWidget` with a `Swipeable`, `Dismissible`
|
||||||
|
or a custom widget to achieve the swipe to reply behaviour.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Migration from onMessageSwiped to Swipeable.
|
||||||
|
StreamMessageListView(
|
||||||
|
...,
|
||||||
|
messageBuilder: (context, messageDetails, messages, defaultWidget) {
|
||||||
|
// The threshold after which the message should be considered as swiped.
|
||||||
|
const threshold = 0.2;
|
||||||
|
|
||||||
|
// The direction in which the message should be swiped to reply.
|
||||||
|
final swipeDirection = messageDetails.isMyMessage
|
||||||
|
? SwipeDirection.endToStart //
|
||||||
|
: SwipeDirection.startToEnd;
|
||||||
|
|
||||||
|
return Swipeable(
|
||||||
|
key: ValueKey(messageDetails.message.id),
|
||||||
|
direction: swipeDirection,
|
||||||
|
swipeThreshold: threshold,
|
||||||
|
onSwiped: (direction) {
|
||||||
|
// Handle the swipe action here.
|
||||||
|
},
|
||||||
|
backgroundBuilder: (context, details) {
|
||||||
|
// The alignment of the swipe action.
|
||||||
|
final alignment = messageDetails.isMyMessage
|
||||||
|
? Alignment.centerRight //
|
||||||
|
: Alignment.centerLeft;
|
||||||
|
|
||||||
|
// The progress of the swipe action.
|
||||||
|
final progress = math.min(details.progress, threshold) / threshold;
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: progress,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.reply,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: defaultWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- Deprecated `StreamMessageWidget.showReactionPickerIndicator` in favor of `StreamMessageWidget.showReactionPicker`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
StreamMessageWidget(
|
||||||
|
- showReactionPickerIndicator: true/false,
|
||||||
|
+ showReactionPicker: true/false,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- Updated `video_player` dependency to `^2.7.0`.
|
||||||
|
- Updated `chewie` dependency to `^1.6.0`.
|
||||||
|
- Updated `share_plus` dependency to `^7.0.2`.
|
||||||
|
- Deprecated `StreamUserItem` in favor of `StreamUserListTile`.
|
||||||
|
- Updated `jiffy` dependency to `^6.2.1`.
|
||||||
|
|
||||||
## 6.4.0
|
## 6.4.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
@@ -80,21 +182,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),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29,21 +29,14 @@ dependencies:
|
|||||||
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
dependency_overrides:
|
|
||||||
stream_chat_flutter:
|
|
||||||
path: ../
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
|||||||
+26
-6
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@template streamVideoAttachment}
|
/// {@template streamVideoAttachment}
|
||||||
|
|||||||
@@ -106,9 +106,10 @@ class _ConnectedTitleState extends StatelessWidget {
|
|||||||
style: textStyle,
|
style: textStyle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
final lastActive = otherMember.user?.lastActive ?? DateTime.now();
|
||||||
alternativeWidget = Text(
|
alternativeWidget = Text(
|
||||||
'${context.translations.userLastOnlineText} '
|
'${context.translations.userLastOnlineText} '
|
||||||
'${Jiffy(otherMember.user?.lastActive).fromNow()}',
|
'${Jiffy.parseFromDateTime(lastActive).fromNow()}',
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,16 +349,16 @@ class _Date extends StatelessWidget {
|
|||||||
|
|
||||||
if (lastMessageAt.millisecondsSinceEpoch >=
|
if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay.millisecondsSinceEpoch) {
|
startOfDay.millisecondsSinceEpoch) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).jm;
|
||||||
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay
|
startOfDay
|
||||||
.subtract(const Duration(days: 1))
|
.subtract(const Duration(days: 1))
|
||||||
.millisecondsSinceEpoch) {
|
.millisecondsSinceEpoch) {
|
||||||
stringDate = context.translations.yesterdayLabel;
|
stringDate = context.translations.yesterdayLabel;
|
||||||
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).EEEE;
|
||||||
} else {
|
} else {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).yMd;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).yMd;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@template streamGalleryFooter}
|
/// {@template streamGalleryFooter}
|
||||||
|
|||||||
@@ -611,13 +611,15 @@ class DefaultTranslations implements Translations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'yesterday';
|
return 'yesterday';
|
||||||
} else {
|
} else {
|
||||||
return 'on ${Jiffy(date).MMMd}';
|
return 'on ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Sent ${_getDay(date)} at ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Today';
|
String get todayLabel => 'Today';
|
||||||
|
|||||||
+10
-8
@@ -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;
|
||||||
@@ -105,6 +105,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);
|
||||||
|
|
||||||
@@ -115,12 +119,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 &&
|
if (widget.showReactionPicker && hasReactionPermission)
|
||||||
(widget.message.status == MessageSendingStatus.sent))
|
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Align(
|
return Align(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/message_input/clear_input_item_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
@@ -383,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.
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart'
|
|
||||||
hide ErrorListener;
|
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart';
|
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/attachment_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/attachment_button.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/command_button.dart';
|
import 'package:stream_chat_flutter/src/message_input/command_button.dart';
|
||||||
@@ -19,7 +15,6 @@ import 'package:stream_chat_flutter/src/message_input/quoted_message_widget.dart
|
|||||||
import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart';
|
import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart';
|
import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_input/tld.dart';
|
import 'package:stream_chat_flutter/src/message_input/tld.dart';
|
||||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
const _kCommandTrigger = '/';
|
const _kCommandTrigger = '/';
|
||||||
@@ -110,6 +105,12 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.messageInputController,
|
this.messageInputController,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
|
this.attachmentListBuilder,
|
||||||
|
this.fileAttachmentListBuilder,
|
||||||
|
this.mediaAttachmentListBuilder,
|
||||||
|
this.fileAttachmentBuilder,
|
||||||
|
this.mediaAttachmentBuilder,
|
||||||
|
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.sendButtonLocation = SendButtonLocation.outside,
|
this.sendButtonLocation = SendButtonLocation.outside,
|
||||||
@@ -130,6 +131,7 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
this.sendButtonBuilder,
|
this.sendButtonBuilder,
|
||||||
this.quotedMessageBuilder,
|
this.quotedMessageBuilder,
|
||||||
|
this.quotedMessageAttachmentThumbnailBuilders,
|
||||||
this.shouldKeepFocusAfterMessage,
|
this.shouldKeepFocusAfterMessage,
|
||||||
this.validator = _defaultValidator,
|
this.validator = _defaultValidator,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
@@ -145,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
|
||||||
@@ -209,9 +210,41 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// The location of the custom actions.
|
/// The location of the custom actions.
|
||||||
final ActionsLocation actionsLocation;
|
final ActionsLocation actionsLocation;
|
||||||
|
|
||||||
|
/// Builder used to build the attachment list present in the message input.
|
||||||
|
///
|
||||||
|
/// In case you want to customize only sub-parts of the attachment list,
|
||||||
|
/// consider using [fileAttachmentListBuilder], [mediaAttachmentListBuilder].
|
||||||
|
final AttachmentListBuilder? attachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file type attachment list.
|
||||||
|
///
|
||||||
|
/// In case you want to customize the attachment item, consider using
|
||||||
|
/// [fileAttachmentBuilder].
|
||||||
|
final AttachmentListBuilder? fileAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media type attachment list.
|
||||||
|
///
|
||||||
|
/// In case you want to customize the attachment item, consider using
|
||||||
|
/// [mediaAttachmentBuilder].
|
||||||
|
final AttachmentListBuilder? mediaAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment item.
|
||||||
|
final AttachmentItemBuilder? fileAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment item.
|
||||||
|
final AttachmentItemBuilder? mediaAttachmentBuilder;
|
||||||
|
|
||||||
/// Map that defines a thumbnail builder for an attachment type.
|
/// Map that defines a thumbnail builder for an attachment type.
|
||||||
|
@Deprecated('Use `mediaAttachmentBuilder` instead.')
|
||||||
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
final Map<String, AttachmentThumbnailBuilder>? attachmentThumbnailBuilders;
|
||||||
|
|
||||||
|
/// Map that defines a thumbnail builder for an attachment type.
|
||||||
|
///
|
||||||
|
/// This is used to build the thumbnail for the attachment in the quoted
|
||||||
|
/// message.
|
||||||
|
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||||
|
quotedMessageAttachmentThumbnailBuilders;
|
||||||
|
|
||||||
/// The focus node associated to the TextField.
|
/// The focus node associated to the TextField.
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
@@ -307,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,
|
||||||
@@ -875,8 +905,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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1133,207 +1161,114 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
|
|
||||||
Widget _buildReplyToMessage() {
|
Widget _buildReplyToMessage() {
|
||||||
if (!_hasQuotedMessage) return const Offstage();
|
if (!_hasQuotedMessage) return const Offstage();
|
||||||
final containsUrl = _effectiveController.message.quotedMessage!.attachments
|
final quotedMessage = _effectiveController.message.quotedMessage!;
|
||||||
.any((element) => element.titleLink != null);
|
|
||||||
|
|
||||||
return widget.quotedMessageBuilder?.call(
|
final quotedMessageBuilder = widget.quotedMessageBuilder;
|
||||||
context,
|
if (quotedMessageBuilder != null) {
|
||||||
_effectiveController.message.quotedMessage!,
|
return quotedMessageBuilder(
|
||||||
) ??
|
context,
|
||||||
StreamQuotedMessageWidget(
|
_effectiveController.message.quotedMessage!,
|
||||||
reverse: true,
|
);
|
||||||
showBorder: !containsUrl,
|
}
|
||||||
message: _effectiveController.message.quotedMessage!,
|
|
||||||
messageTheme: _streamChatTheme.otherMessageTheme,
|
final containsUrl = quotedMessage.attachments.any((it) {
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
return it.titleLink != null;
|
||||||
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
});
|
||||||
attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders,
|
|
||||||
);
|
return StreamQuotedMessageWidget(
|
||||||
|
reverse: true,
|
||||||
|
showBorder: !containsUrl,
|
||||||
|
message: quotedMessage,
|
||||||
|
messageTheme: _streamChatTheme.otherMessageTheme,
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
|
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
||||||
|
attachmentThumbnailBuilders:
|
||||||
|
widget.quotedMessageAttachmentThumbnailBuilders,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
final nonOGAttachments = _effectiveController.attachments.where(
|
final attachments = _effectiveController.attachments;
|
||||||
(it) => it.titleLink == null,
|
final nonOGAttachments = attachments.where((it) {
|
||||||
);
|
return it.titleLink == null;
|
||||||
|
}).toList(growable: false);
|
||||||
|
|
||||||
|
// If there are no attachments, return an empty widget
|
||||||
if (nonOGAttachments.isEmpty) return const Offstage();
|
if (nonOGAttachments.isEmpty) return const Offstage();
|
||||||
final fileAttachments = nonOGAttachments
|
|
||||||
.where((it) => it.type == 'file')
|
// If the user has provided a custom attachment list builder, use that.
|
||||||
.toList(growable: false);
|
final attachmentListBuilder = widget.attachmentListBuilder;
|
||||||
final remainingAttachments = nonOGAttachments
|
if (attachmentListBuilder != null) {
|
||||||
.where((it) => it.type != 'file')
|
return attachmentListBuilder(
|
||||||
.toList(growable: false);
|
context,
|
||||||
return Column(
|
nonOGAttachments,
|
||||||
children: [
|
_onAttachmentRemovePressed,
|
||||||
if (fileAttachments.isNotEmpty)
|
);
|
||||||
Padding(
|
}
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
|
||||||
child: LimitedBox(
|
// Otherwise, use the default attachment list builder.
|
||||||
maxHeight: 136,
|
return LimitedBox(
|
||||||
child: ListView(
|
maxHeight: 240,
|
||||||
reverse: true,
|
child: StreamMessageInputAttachmentList(
|
||||||
shrinkWrap: true,
|
attachments: nonOGAttachments,
|
||||||
children: fileAttachments.reversed
|
onRemovePressed: _onAttachmentRemovePressed,
|
||||||
.map<Widget>(
|
fileAttachmentListBuilder: widget.fileAttachmentListBuilder,
|
||||||
(e) => ClipRRect(
|
mediaAttachmentListBuilder: widget.mediaAttachmentListBuilder,
|
||||||
borderRadius: BorderRadius.circular(10),
|
fileAttachmentBuilder: widget.fileAttachmentBuilder,
|
||||||
child: StreamFileAttachment(
|
mediaAttachmentBuilder: widget.mediaAttachmentBuilder ??
|
||||||
message: Message(), // dummy message
|
// For backward compatibility.
|
||||||
attachment: e,
|
// TODO: Remove in the next major release.
|
||||||
constraints: BoxConstraints.loose(Size(
|
(context, attachment, onRemovePressed) {
|
||||||
MediaQuery.of(context).size.width * 0.65,
|
final Widget mediaAttachmentThumbnail;
|
||||||
56,
|
|
||||||
)),
|
final builder =
|
||||||
trailing: Padding(
|
widget.attachmentThumbnailBuilders?[attachment.type];
|
||||||
padding: const EdgeInsets.all(8),
|
if (builder != null) {
|
||||||
child: _buildRemoveButton(e),
|
mediaAttachmentThumbnail = builder(context, attachment);
|
||||||
),
|
} else {
|
||||||
),
|
mediaAttachmentThumbnail = MessageInputMediaAttachmentThumbnail(
|
||||||
|
attachment: attachment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: mediaAttachmentThumbnail,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed(attachment)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.insertBetween(const SizedBox(height: 8)),
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
if (remainingAttachments.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
|
||||||
child: LimitedBox(
|
|
||||||
maxHeight: 104,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: remainingAttachments
|
|
||||||
.map<Widget>(
|
|
||||||
(attachment) => ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 104,
|
|
||||||
width: 104,
|
|
||||||
child: _buildAttachment(attachment),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: _buildRemoveButton(attachment),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.insertBetween(const SizedBox(width: 8)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRemoveButton(Attachment attachment) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: RawMaterialButton(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
highlightElevation: 0,
|
|
||||||
focusElevation: 0,
|
|
||||||
hoverElevation: 0,
|
|
||||||
onPressed: () async {
|
|
||||||
final file = attachment.file;
|
|
||||||
final uploadState = attachment.uploadState;
|
|
||||||
|
|
||||||
if (file != null && !uploadState.isSuccess && !isWeb) {
|
|
||||||
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
|
||||||
attachmentFile: file,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_effectiveController.removeAttachmentById(attachment.id);
|
|
||||||
},
|
|
||||||
fillColor:
|
|
||||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
|
||||||
child: Center(
|
|
||||||
child: StreamSvgIcon.close(
|
|
||||||
size: 24,
|
|
||||||
color: _streamChatTheme.colorTheme.barsBg,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachment(Attachment attachment) {
|
// Default callback for removing an attachment.
|
||||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
Future<void> _onAttachmentRemovePressed(Attachment attachment) async {
|
||||||
true) {
|
final file = attachment.file;
|
||||||
return widget.attachmentThumbnailBuilders![attachment.type!]!(
|
final uploadState = attachment.uploadState;
|
||||||
context,
|
|
||||||
attachment,
|
if (file != null && !uploadState.isSuccess && !isWeb) {
|
||||||
|
await StreamAttachmentHandler.instance.deleteAttachmentFile(
|
||||||
|
attachmentFile: file,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (attachment.type) {
|
_effectiveController.removeAttachmentById(attachment.id);
|
||||||
case 'image':
|
|
||||||
case 'giphy':
|
|
||||||
return attachment.file != null
|
|
||||||
? Image.memory(
|
|
||||||
attachment.file!.bytes!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, _, __) => Image.asset(
|
|
||||||
'images/placeholder.png',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: CachedNetworkImage(
|
|
||||||
imageUrl: attachment.imageUrl ??
|
|
||||||
attachment.assetUrl ??
|
|
||||||
attachment.thumbUrl!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorWidget: (_, obj, trace) =>
|
|
||||||
getFileTypeImage(attachment.extraData['other'] as String?),
|
|
||||||
placeholder: (context, _) => Shimmer.fromColors(
|
|
||||||
baseColor: _streamChatTheme.colorTheme.disabled,
|
|
||||||
highlightColor: _streamChatTheme.colorTheme.inputBg,
|
|
||||||
child: Image.asset(
|
|
||||||
'images/placeholder.png',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case 'video':
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
StreamVideoThumbnailImage(
|
|
||||||
constraints: BoxConstraints.loose(
|
|
||||||
const Size(
|
|
||||||
104,
|
|
||||||
104,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
video: attachment.file?.path ?? attachment.assetUrl,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 8,
|
|
||||||
bottom: 10,
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
'svgs/video_call_icon.svg',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return const ColoredBox(
|
|
||||||
color: Colors.black26,
|
|
||||||
child: Icon(Icons.insert_drive_file),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton(BuildContext context) {
|
Widget _buildCommandButton(BuildContext context) {
|
||||||
|
|||||||
+388
@@ -0,0 +1,388 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/attachment/attachment.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// WidgetBuilder used to build the message input attachment list.
|
||||||
|
///
|
||||||
|
/// see more:
|
||||||
|
/// - [StreamMessageInputAttachmentList]
|
||||||
|
typedef AttachmentListBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<Attachment> attachments,
|
||||||
|
ValueSetter<Attachment>? onRemovePressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// WidgetBuilder used to build the message input attachment item.
|
||||||
|
///
|
||||||
|
/// see more:
|
||||||
|
/// - [StreamMessageInputAttachmentList]
|
||||||
|
typedef AttachmentItemBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Attachment attachment,
|
||||||
|
ValueSetter<Attachment>? onRemovePressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template stream_message_input_attachment_list}
|
||||||
|
/// Widget used to display the list of attachments added to the message input.
|
||||||
|
///
|
||||||
|
/// By default, it displays the list of file attachments and media attachments
|
||||||
|
/// separately.
|
||||||
|
///
|
||||||
|
/// You can customize the list of file attachments and media attachments using
|
||||||
|
/// [fileAttachmentListBuilder] and [mediaAttachmentListBuilder] respectively.
|
||||||
|
///
|
||||||
|
/// You can also customize the attachment item using [fileAttachmentBuilder] and
|
||||||
|
/// [mediaAttachmentBuilder] respectively.
|
||||||
|
///
|
||||||
|
/// You can override the default action of removing an attachment by providing
|
||||||
|
/// [onRemovePressed].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class StreamMessageInputAttachmentList extends StatefulWidget {
|
||||||
|
/// {@macro stream_message_input_attachment_list}
|
||||||
|
const StreamMessageInputAttachmentList({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.onRemovePressed,
|
||||||
|
this.fileAttachmentBuilder,
|
||||||
|
this.mediaAttachmentBuilder,
|
||||||
|
this.fileAttachmentListBuilder,
|
||||||
|
this.mediaAttachmentListBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of attachments to display thumbnails for.
|
||||||
|
///
|
||||||
|
/// Open graph should be filtered out.
|
||||||
|
final Iterable<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment item.
|
||||||
|
final AttachmentItemBuilder? fileAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment item.
|
||||||
|
final AttachmentItemBuilder? mediaAttachmentBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the file attachment list.
|
||||||
|
final AttachmentListBuilder? fileAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Builder used to build the media attachment list.
|
||||||
|
final AttachmentListBuilder? mediaAttachmentListBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StreamMessageInputAttachmentList> createState() =>
|
||||||
|
_StreamMessageInputAttachmentListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StreamMessageInputAttachmentListState
|
||||||
|
extends State<StreamMessageInputAttachmentList> {
|
||||||
|
List<Attachment> fileAttachments = [];
|
||||||
|
List<Attachment> mediaAttachments = [];
|
||||||
|
|
||||||
|
void _updateAttachments() {
|
||||||
|
// Clear the lists.
|
||||||
|
fileAttachments.clear();
|
||||||
|
mediaAttachments.clear();
|
||||||
|
|
||||||
|
// Split the attachments into file and media attachments.
|
||||||
|
for (final attachment in widget.attachments) {
|
||||||
|
if (attachment.type == 'file') {
|
||||||
|
fileAttachments.add(attachment);
|
||||||
|
} else {
|
||||||
|
mediaAttachments.add(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateAttachments();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant StreamMessageInputAttachmentList oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.attachments != widget.attachments) {
|
||||||
|
_updateAttachments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If there are no attachments, return an empty box.
|
||||||
|
if (fileAttachments.isEmpty && mediaAttachments.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (mediaAttachments.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: widget.mediaAttachmentListBuilder?.call(
|
||||||
|
context,
|
||||||
|
mediaAttachments,
|
||||||
|
widget.onRemovePressed,
|
||||||
|
) ??
|
||||||
|
MessageInputMediaAttachments(
|
||||||
|
attachments: mediaAttachments,
|
||||||
|
attachmentBuilder: widget.mediaAttachmentBuilder,
|
||||||
|
onRemovePressed: widget.onRemovePressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (fileAttachments.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: widget.fileAttachmentListBuilder?.call(
|
||||||
|
context,
|
||||||
|
fileAttachments,
|
||||||
|
widget.onRemovePressed,
|
||||||
|
) ??
|
||||||
|
MessageInputFileAttachments(
|
||||||
|
attachments: fileAttachments,
|
||||||
|
attachmentBuilder: widget.fileAttachmentBuilder,
|
||||||
|
onRemovePressed: widget.onRemovePressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].insertBetween(
|
||||||
|
Divider(
|
||||||
|
height: 16,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
thickness: 1,
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget used to display the list of file type attachments added to the
|
||||||
|
/// message input.
|
||||||
|
class MessageInputFileAttachments extends StatelessWidget {
|
||||||
|
/// Creates a new FileAttachments widget.
|
||||||
|
const MessageInputFileAttachments({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.attachmentBuilder,
|
||||||
|
this.onRemovePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of file type attachments to display thumbnails for.
|
||||||
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the file type attachment item.
|
||||||
|
final AttachmentItemBuilder? attachmentBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
reverse: true,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
children: attachments.reversed.map<Widget>(
|
||||||
|
(attachment) {
|
||||||
|
// If a custom builder is provided, use it.
|
||||||
|
final builder = attachmentBuilder;
|
||||||
|
if (builder != null) {
|
||||||
|
return builder(context, attachment, onRemovePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the default builder.
|
||||||
|
return ClipRRect(
|
||||||
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: StreamFileAttachment(
|
||||||
|
message: Message(), // dummy message
|
||||||
|
attachment: attachment,
|
||||||
|
constraints: BoxConstraints.loose(Size(
|
||||||
|
MediaQuery.sizeOf(context).width * 0.65,
|
||||||
|
56,
|
||||||
|
)),
|
||||||
|
trailing: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed!(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).insertBetween(const SizedBox(height: 8)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget used to display the list of media type attachments added to the
|
||||||
|
/// message input.
|
||||||
|
class MessageInputMediaAttachments extends StatelessWidget {
|
||||||
|
/// Creates a new MediaAttachments widget.
|
||||||
|
const MessageInputMediaAttachments({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
this.attachmentBuilder,
|
||||||
|
this.onRemovePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of media type attachments to display thumbnails for.
|
||||||
|
///
|
||||||
|
/// Only attachments of type `image`, `video` and `giphy` are supported. Shows
|
||||||
|
/// a placeholder for other types of attachments.
|
||||||
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
|
/// Builder used to build the media type attachment item.
|
||||||
|
final AttachmentItemBuilder? attachmentBuilder;
|
||||||
|
|
||||||
|
/// Callback called when the remove button is pressed.
|
||||||
|
final ValueSetter<Attachment>? onRemovePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 104,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
children: attachments.map<Widget>(
|
||||||
|
(attachment) {
|
||||||
|
// If a custom builder is provided, use it.
|
||||||
|
final builder = attachmentBuilder;
|
||||||
|
if (builder != null) {
|
||||||
|
return builder(context, attachment, onRemovePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
key: Key(attachment.id),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: MessageInputMediaAttachmentThumbnail(
|
||||||
|
attachment: attachment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: RemoveAttachmentButton(
|
||||||
|
onPressed: onRemovePressed != null
|
||||||
|
? () => onRemovePressed!(attachment)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).insertBetween(const SizedBox(width: 8)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays a thumbnail for a media attachment.
|
||||||
|
class MessageInputMediaAttachmentThumbnail extends StatelessWidget {
|
||||||
|
/// Creates a new media attachment widget.
|
||||||
|
const MessageInputMediaAttachmentThumbnail({
|
||||||
|
super.key,
|
||||||
|
required this.attachment,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The attachment to display.
|
||||||
|
final Attachment attachment;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (attachment.type) {
|
||||||
|
case 'image':
|
||||||
|
case 'giphy':
|
||||||
|
return attachment.file != null
|
||||||
|
? Image.memory(
|
||||||
|
attachment.file!.bytes!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, _, __) => Image.asset(
|
||||||
|
'images/placeholder.png',
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: CachedNetworkImage(
|
||||||
|
imageUrl: attachment.imageUrl ??
|
||||||
|
attachment.assetUrl ??
|
||||||
|
attachment.thumbUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: (_, obj, trace) => Image.asset(
|
||||||
|
'images/placeholder.png',
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 'video':
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
StreamVideoThumbnailImage(
|
||||||
|
video: attachment.file?.path ?? attachment.assetUrl,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
bottom: 10,
|
||||||
|
child: StreamSvgIcon.videoCall(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const ColoredBox(
|
||||||
|
color: Colors.black26,
|
||||||
|
child: Icon(Icons.insert_drive_file),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Material Button used for removing attachments.
|
||||||
|
class RemoveAttachmentButton extends StatelessWidget {
|
||||||
|
/// Creates a new remove attachment button.
|
||||||
|
const RemoveAttachmentButton({super.key, this.onPressed});
|
||||||
|
|
||||||
|
/// Callback when the remove attachment button is pressed.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
final colorTheme = theme.colorTheme;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: RawMaterialButton(
|
||||||
|
elevation: 0,
|
||||||
|
focusElevation: 0,
|
||||||
|
hoverElevation: 0,
|
||||||
|
highlightElevation: 0,
|
||||||
|
onPressed: onPressed,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
fillColor: colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
|
child: StreamSvgIcon.close(
|
||||||
|
size: 24,
|
||||||
|
color: colorTheme.barsBg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -664,14 +669,20 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final isPartOfThread = message.replyCount! > 0 ||
|
final isPartOfThread = message.replyCount! > 0 ||
|
||||||
message.showInChannel == true;
|
message.showInChannel == true;
|
||||||
|
|
||||||
final createdAt = message.createdAt.toLocal();
|
final createdAt = Jiffy.parseFromDateTime(
|
||||||
final nextCreatedAt = nextMessage.createdAt.toLocal();
|
message.createdAt.toLocal(),
|
||||||
if (!Jiffy(createdAt).isSame(nextCreatedAt, Units.DAY)) {
|
);
|
||||||
|
|
||||||
|
final nextCreatedAt = Jiffy.parseFromDateTime(
|
||||||
|
nextMessage.createdAt.toLocal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createdAt.isSame(nextCreatedAt, unit: Unit.day)) {
|
||||||
separator = _buildDateDivider(nextMessage);
|
separator = _buildDateDivider(nextMessage);
|
||||||
} else {
|
} else {
|
||||||
final hasTimeDiff = !Jiffy(createdAt).isSame(
|
final hasTimeDiff = !createdAt.isSame(
|
||||||
nextCreatedAt,
|
nextCreatedAt,
|
||||||
Units.MINUTE,
|
unit: Unit.minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
@@ -1066,10 +1077,12 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
|
|
||||||
var hasTimeDiff = false;
|
var hasTimeDiff = false;
|
||||||
if (nextMessage != null) {
|
if (nextMessage != null) {
|
||||||
hasTimeDiff = !Jiffy(message.createdAt.toLocal()).isSame(
|
final createdAt = Jiffy.parseFromDateTime(message.createdAt.toLocal());
|
||||||
|
final nextCreatedAt = Jiffy.parseFromDateTime(
|
||||||
nextMessage.createdAt.toLocal(),
|
nextMessage.createdAt.toLocal(),
|
||||||
Units.MINUTE,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
hasTimeDiff = !createdAt.isSame(nextCreatedAt, unit: Unit.minute);
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasFileAttachment =
|
final hasFileAttachment =
|
||||||
@@ -1226,26 +1239,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 +1262,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class BottomRow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (showTimeStamp)
|
if (showTimeStamp)
|
||||||
Text(
|
Text(
|
||||||
Jiffy(message.createdAt.toLocal()).jm,
|
Jiffy.parseFromDateTime(message.createdAt.toLocal()).jm,
|
||||||
style: messageTheme.createdAtStyle,
|
style: messageTheme.createdAtStyle,
|
||||||
),
|
),
|
||||||
if (showSendingIndicator)
|
if (showSendingIndicator)
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -885,12 +908,12 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
if (!widget.message.isDeleted) {
|
if (!widget.message.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,
|
||||||
@@ -963,9 +986,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,
|
||||||
@@ -996,14 +1019,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(),
|
||||||
@@ -1149,21 +1173,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void onLongPress(BuildContext context) {
|
void _showMessageReactionsModal(BuildContext context) {
|
||||||
if (widget.message.isEphemeral ||
|
|
||||||
widget.message.status == MessageSendingStatus.sending) {
|
|
||||||
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(
|
||||||
@@ -1173,7 +1183,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(
|
||||||
@@ -1187,36 +1198,93 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
showReactionPickerIndicator: widget.showReactions &&
|
// Show the tail if the reaction picker is visible.
|
||||||
(widget.message.status == MessageSendingStatus.sent),
|
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,
|
|
||||||
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.status == MessageSendingStatus.sending) {
|
||||||
|
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.status == MessageSendingStatus.sent),
|
|
||||||
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,
|
||||||
|
|||||||
+4
-5
@@ -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.status == MessageSendingStatus.sent))
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class SendingIndicatorBuilder extends StatelessWidget {
|
|||||||
Widget child = StreamSendingIndicator(
|
Widget child = StreamSendingIndicator(
|
||||||
message: message,
|
message: message,
|
||||||
isMessageRead: isMessageRead,
|
isMessageRead: isMessageRead,
|
||||||
size: style!.fontSize,
|
size: style?.fontSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMessageRead) {
|
if (isMessageRead) {
|
||||||
@@ -86,7 +86,7 @@ class SendingIndicatorBuilder extends StatelessWidget {
|
|||||||
if (memberCount > 2)
|
if (memberCount > 2)
|
||||||
Text(
|
Text(
|
||||||
readList.length.toString(),
|
readList.length.toString(),
|
||||||
style: style.copyWith(
|
style: style?.copyWith(
|
||||||
color: streamChatTheme.colorTheme.accentPrimary,
|
color: streamChatTheme.colorTheme.accentPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,17 +20,17 @@ class StreamDateDivider extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final createdAt = Jiffy(dateTime);
|
final createdAt = Jiffy.parseFromDateTime(dateTime);
|
||||||
final now = Jiffy(DateTime.now());
|
final now = Jiffy.parseFromDateTime(DateTime.now());
|
||||||
|
|
||||||
var dayInfo = createdAt.MMMd;
|
var dayInfo = createdAt.MMMd;
|
||||||
if (createdAt.isSame(now, Units.DAY)) {
|
if (createdAt.isSame(now, unit: Unit.day)) {
|
||||||
dayInfo = context.translations.todayLabel;
|
dayInfo = context.translations.todayLabel;
|
||||||
} else if (createdAt.isSame(now.subtract(days: 1), Units.DAY)) {
|
} else if (createdAt.isSame(now.subtract(days: 1), unit: Unit.day)) {
|
||||||
dayInfo = context.translations.yesterdayLabel;
|
dayInfo = context.translations.yesterdayLabel;
|
||||||
} else if (createdAt.isAfter(now.subtract(days: 7), Units.DAY)) {
|
} else if (createdAt.isAfter(now.subtract(days: 7), unit: Unit.day)) {
|
||||||
dayInfo = createdAt.EEEE;
|
dayInfo = createdAt.EEEE;
|
||||||
} else if (createdAt.isAfter(now.subtract(years: 1), Units.DAY)) {
|
} else if (createdAt.isAfter(now.subtract(years: 1), unit: Unit.day)) {
|
||||||
dayInfo = createdAt.MMMd;
|
dayInfo = createdAt.MMMd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -287,16 +287,16 @@ class ChannelLastMessageDate extends StatelessWidget {
|
|||||||
|
|
||||||
if (lastMessageAt.millisecondsSinceEpoch >=
|
if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay.millisecondsSinceEpoch) {
|
startOfDay.millisecondsSinceEpoch) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).jm;
|
||||||
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
||||||
startOfDay
|
startOfDay
|
||||||
.subtract(const Duration(days: 1))
|
.subtract(const Duration(days: 1))
|
||||||
.millisecondsSinceEpoch) {
|
.millisecondsSinceEpoch) {
|
||||||
stringDate = context.translations.yesterdayLabel;
|
stringDate = context.translations.yesterdayLabel;
|
||||||
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).EEEE;
|
||||||
} else {
|
} else {
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).yMd;
|
stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).yMd;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
+2
-2
@@ -227,9 +227,9 @@ class MessageSearchTileMessageDate extends StatelessWidget {
|
|||||||
if (now.year != createdAt.year ||
|
if (now.year != createdAt.year ||
|
||||||
now.month != createdAt.month ||
|
now.month != createdAt.month ||
|
||||||
now.day != createdAt.day) {
|
now.day != createdAt.day) {
|
||||||
stringDate = Jiffy(createdAt.toLocal()).yMd;
|
stringDate = Jiffy.parseFromDateTime(createdAt.toLocal()).yMd;
|
||||||
} else {
|
} else {
|
||||||
stringDate = Jiffy(createdAt.toLocal()).jm;
|
stringDate = Jiffy.parseFromDateTime(createdAt.toLocal()).jm;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
+2
-1
@@ -176,11 +176,12 @@ class UserLastActive extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final chatTheme = StreamChatTheme.of(context);
|
final chatTheme = StreamChatTheme.of(context);
|
||||||
|
final lastActive = user.lastActive ?? DateTime.now();
|
||||||
return Text(
|
return Text(
|
||||||
user.online
|
user.online
|
||||||
? context.translations.userOnlineText
|
? context.translations.userOnlineText
|
||||||
: '${context.translations.userLastOnlineText} '
|
: '${context.translations.userLastOnlineText} '
|
||||||
'${Jiffy(user.lastActive).fromNow()}',
|
'${Jiffy.parseFromDateTime(lastActive).fromNow()}',
|
||||||
style: chatTheme.textTheme.footnote.copyWith(
|
style: chatTheme.textTheme.footnote.copyWith(
|
||||||
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -165,9 +165,9 @@ class StreamChatState extends State<StreamChat> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
final currentLocale = Localizations.localeOf(context).toString();
|
final currentLocale = Localizations.localeOf(context).toString();
|
||||||
final availableLocales = Jiffy.getAllAvailableLocales();
|
final availableLocales = Jiffy.getSupportedLocales();
|
||||||
if (availableLocales.contains(currentLocale)) {
|
if (availableLocales.contains(currentLocale)) {
|
||||||
Jiffy.locale(currentLocale);
|
Jiffy.setLocale(currentLocale);
|
||||||
}
|
}
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -73,11 +74,12 @@ class StreamUserItem extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildLastActive(BuildContext context) {
|
Widget _buildLastActive(BuildContext context) {
|
||||||
final chatTheme = StreamChatTheme.of(context);
|
final chatTheme = StreamChatTheme.of(context);
|
||||||
|
final lastActive = user.lastActive ?? DateTime.now();
|
||||||
return Text(
|
return Text(
|
||||||
user.online
|
user.online
|
||||||
? context.translations.userOnlineText
|
? context.translations.userOnlineText
|
||||||
: '${context.translations.userLastOnlineText} '
|
: '${context.translations.userLastOnlineText} '
|
||||||
'${Jiffy(user.lastActive).fromNow()}',
|
'${Jiffy.parseFromDateTime(lastActive).fromNow()}',
|
||||||
style: chatTheme.textTheme.footnote.copyWith(
|
style: chatTheme.textTheme.footnote.copyWith(
|
||||||
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export 'src/message_input/attachment_picker/stream_attachment_picker_bottom_shee
|
|||||||
export 'src/message_input/countdown_button.dart';
|
export 'src/message_input/countdown_button.dart';
|
||||||
export 'src/message_input/enums.dart';
|
export 'src/message_input/enums.dart';
|
||||||
export 'src/message_input/stream_message_input.dart';
|
export 'src/message_input/stream_message_input.dart';
|
||||||
|
export 'src/message_input/stream_message_input_attachment_list.dart';
|
||||||
export 'src/message_input/stream_message_send_button.dart';
|
export 'src/message_input/stream_message_send_button.dart';
|
||||||
export 'src/message_input/stream_message_text_field.dart';
|
export 'src/message_input/stream_message_text_field.dart';
|
||||||
export 'src/message_list_view/message_details.dart';
|
export 'src/message_list_view/message_details.dart';
|
||||||
@@ -57,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';
|
||||||
@@ -65,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';
|
||||||
@@ -96,3 +99,4 @@ export 'src/utils/device_segmentation.dart';
|
|||||||
export 'src/utils/extensions.dart';
|
export 'src/utils/extensions.dart';
|
||||||
export 'src/utils/helpers.dart';
|
export 'src/utils/helpers.dart';
|
||||||
export 'src/utils/typedefs.dart';
|
export 'src/utils/typedefs.dart';
|
||||||
|
export 'src/video/video_thumbnail_image.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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.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
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
cached_network_image: ^3.0.0
|
cached_network_image: ^3.0.0
|
||||||
chewie: ^1.3.4
|
chewie: ^1.6.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
|
||||||
@@ -28,21 +28,21 @@ dependencies:
|
|||||||
flutter_svg: ^2.0.4
|
flutter_svg: ^2.0.4
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
image_gallery_saver: ^2.0.1
|
image_gallery_saver: ^2.0.1
|
||||||
image_picker: ^0.8.2
|
image_picker: ">=0.8.2 <2.0.0"
|
||||||
jiffy: ^5.0.0
|
jiffy: ^6.2.1
|
||||||
lottie: ^2.0.0
|
lottie: ^2.0.0
|
||||||
meta: ^1.8.0
|
meta: ^1.8.0
|
||||||
path_provider: ^2.0.9
|
path_provider: ^2.0.9
|
||||||
photo_manager: ^2.5.2
|
photo_manager: ^2.5.2
|
||||||
photo_view: ^0.14.0
|
photo_view: ^0.14.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
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.4.0
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
thumblr: ^0.0.4
|
thumblr: ^0.0.4
|
||||||
url_launcher: ^6.1.0
|
url_launcher: ^6.1.0
|
||||||
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.0
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ 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
|
||||||
|
|||||||
+190
@@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
+1
-1
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -27,7 +27,7 @@ dependencies:
|
|||||||
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
dart_code_metrics: ^5.7.2
|
|
||||||
fake_async: ^1.2.0
|
fake_async: ^1.2.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 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).
|
||||||
|
|||||||
@@ -270,13 +270,15 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'yesterday';
|
return 'yesterday';
|
||||||
} else {
|
} else {
|
||||||
return 'on ${Jiffy(date).MMMd}';
|
return 'on ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Sent ${_getDay(date)} at ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Today';
|
String get todayLabel => 'Today';
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
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.17.0 <4.0.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -249,13 +249,15 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'ahir';
|
return 'ahir';
|
||||||
} else {
|
} else {
|
||||||
return 'el ${Jiffy(date).MMMd}';
|
return 'el ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'''Enviat el ${_getDay(date)} a les ${Jiffy(time.toLocal()).format('HH:mm')}''';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Enviat el ${_getDay(date)} a les ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Avui';
|
String get todayLabel => 'Avui';
|
||||||
|
|||||||
@@ -239,13 +239,15 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'Gestern';
|
return 'Gestern';
|
||||||
} else {
|
} else {
|
||||||
return 'am ${Jiffy(date).MMMd}';
|
return 'am ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Gesendet ${_getDay(date)} am ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Gesendet ${_getDay(date)} am ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Heute';
|
String get todayLabel => 'Heute';
|
||||||
|
|||||||
@@ -246,13 +246,15 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'yesterday';
|
return 'yesterday';
|
||||||
} else {
|
} else {
|
||||||
return 'on ${Jiffy(date).MMMd}';
|
return 'on ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Sent ${_getDay(date)} at ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Today';
|
String get todayLabel => 'Today';
|
||||||
|
|||||||
@@ -250,13 +250,15 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'ayer';
|
return 'ayer';
|
||||||
} else {
|
} else {
|
||||||
return 'el ${Jiffy(date).MMMd}';
|
return 'el ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'''Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}''';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Enviado el ${_getDay(date)} a las ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Hoy';
|
String get todayLabel => 'Hoy';
|
||||||
|
|||||||
@@ -249,13 +249,15 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'hier';
|
return 'hier';
|
||||||
} else {
|
} else {
|
||||||
return 'le ${Jiffy(date).MMMd}';
|
return 'le ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Envoyé ${_getDay(date)} à ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Envoyé ${_getDay(date)} à ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => "Aujourd'hui";
|
String get todayLabel => "Aujourd'hui";
|
||||||
|
|||||||
@@ -243,13 +243,15 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'कल';
|
return 'कल';
|
||||||
} else {
|
} else {
|
||||||
return '${Jiffy(date).MMMd} को';
|
return '${Jiffy.parseFromDateTime(date).MMMd} को';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'${_getDay(date)} ${Jiffy(time.toLocal()).format('HH:mm')} बजे भेजा गया';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return '${_getDay(date)} ${atTime.jm} बजे भेजा गया';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'आज';
|
String get todayLabel => 'आज';
|
||||||
|
|||||||
@@ -252,13 +252,15 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'ieri';
|
return 'ieri';
|
||||||
} else {
|
} else {
|
||||||
return 'il ${Jiffy(date).MMMd}';
|
return 'il ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
"Inviato ${_getDay(date)} alle ${Jiffy(time.toLocal()).format('HH:mm')}";
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Inviato ${_getDay(date)} alle ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Oggi';
|
String get todayLabel => 'Oggi';
|
||||||
|
|||||||
@@ -236,13 +236,15 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return '昨日';
|
return '昨日';
|
||||||
} else {
|
} else {
|
||||||
return '${Jiffy(date).MMMd}に';
|
return '${Jiffy.parseFromDateTime(date).MMMd}に';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'${_getDay(date)}の${Jiffy(time.toLocal()).format('HH:mm')}に送信しました ';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return '${_getDay(date)}の${atTime.jm}に送信しました ';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => '今日';
|
String get todayLabel => '今日';
|
||||||
|
|||||||
@@ -235,13 +235,15 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return '어제';
|
return '어제';
|
||||||
} else {
|
} else {
|
||||||
return '${Jiffy(date).MMMd}에';
|
return '${Jiffy.parseFromDateTime(date).MMMd}에';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'${_getDay(date)} ${Jiffy(time.toLocal()).format('HH:mm')}에 보냈습니다';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return '${_getDay(date)} ${atTime.jm}에 보냈습니다';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => '오늘';
|
String get todayLabel => '오늘';
|
||||||
|
|||||||
@@ -242,13 +242,15 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'i går';
|
return 'i går';
|
||||||
} else {
|
} else {
|
||||||
return 'på ${Jiffy(date).MMMd}';
|
return 'på ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'Sent ${_getDay(date)} kl. ${Jiffy(time.toLocal()).format('HH:mm')}';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Sent ${_getDay(date)} kl. ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'I dag';
|
String get todayLabel => 'I dag';
|
||||||
|
|||||||
@@ -244,13 +244,15 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations {
|
|||||||
} else if (date == yesterday) {
|
} else if (date == yesterday) {
|
||||||
return 'Ontem';
|
return 'Ontem';
|
||||||
} else {
|
} else {
|
||||||
return 'o ${Jiffy(date).MMMd}';
|
return 'o ${Jiffy.parseFromDateTime(date).MMMd}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
String sentAtText({required DateTime date, required DateTime time}) {
|
||||||
'''Enviado ${_getDay(date)} às ${Jiffy(time.toLocal()).format('HH:mm')}''';
|
final atTime = Jiffy.parseFromDateTime(time.toLocal());
|
||||||
|
return 'Enviado ${_getDay(date)} às ${atTime.jm}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get todayLabel => 'Hoje';
|
String get todayLabel => 'Hoje';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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.5.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
|
||||||
@@ -14,10 +14,9 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter: ^6.4.0
|
stream_chat_flutter: ^6.5.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);
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ publish_to: 'none'
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
dart_code_metrics: ^5.7.2
|
|
||||||
drift_dev: ^2.7.0
|
drift_dev: ^2.7.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user