Merge branch 'develop' into feat/limit-attachment-selection
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
|
||||
✅ Added
|
||||
|
||||
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for
|
||||
building a widget when the `UserAvatar` image is loading
|
||||
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516):
|
||||
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image
|
||||
is loading
|
||||
- Added a `backgroundColor` property to the following widgets:
|
||||
- `ChannelHeader`
|
||||
- `ChannelListHeader`
|
||||
@@ -13,13 +14,26 @@
|
||||
- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message.
|
||||
- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded.
|
||||
This will override the default error alert behaviour.
|
||||
- Added `MessageInput.attachmentButtonBuilder` and `MessageInput.commandButtonBuilder` for more customizations.
|
||||
|
||||
```dart
|
||||
typedef ActionButtonBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
IconButton defaultActionButton,
|
||||
);
|
||||
```
|
||||
|
||||
> **_NOTE:_** The last parameter is the default `ActionButton`
|
||||
You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
- Added slow mode which allows a cooldown period after a user sends a message.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes
|
||||
associated with them, and have been upgraded with some goodies like `lerp` functions.
|
||||
Here's the full naming breakdown:
|
||||
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
|
||||
them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming
|
||||
breakdown:
|
||||
|
||||
* `AvatarTheme` is now `AvatarThemeData`
|
||||
* `ChannelHeaderTheme` is now `ChannelHeaderThemeData`
|
||||
* `ChannelListHeaderTheme` is now `ChannelListHeaderThemeData`
|
||||
@@ -31,12 +45,23 @@ Here's the full naming breakdown:
|
||||
* `MessageTheme` is now `MessageThemeData`
|
||||
* `UserListViewTheme` is now `UserListViewThemeData`
|
||||
|
||||
- Updated core dependency.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the
|
||||
camera is null.
|
||||
- Fixed date dividers position/alignment in non reversed `MessageListView`.
|
||||
- Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set.
|
||||
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text.
|
||||
|
||||
## 2.1.2
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending message
|
||||
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no
|
||||
members when sending message
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Updated core dependency
|
||||
@@ -53,7 +78,8 @@ Here's the full naming breakdown:
|
||||
🔄 Changed
|
||||
|
||||
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
|
||||
- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
|
||||
- `StreamChat.of(context).userStream` is now deprecated in favor
|
||||
of `StreamChat.of(context).currentUserStream`.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -67,17 +93,17 @@ Here's the full naming breakdown:
|
||||
- Renamed `ChannelImage` to `ChannelAvatar`
|
||||
- Updated `StreamChatThemeData.reactionIcons` to accept custom builder
|
||||
- Renamed `ColorTheme` properties to reflect the purpose of the colors
|
||||
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
|
||||
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
|
||||
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
|
||||
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
|
||||
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
|
||||
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
|
||||
- `ColorTheme.white` -> `ColorTheme.barsBg`
|
||||
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
|
||||
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
|
||||
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
|
||||
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
|
||||
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
|
||||
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
|
||||
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
|
||||
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
|
||||
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
|
||||
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
|
||||
- `ColorTheme.white` -> `ColorTheme.barsBg`
|
||||
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
|
||||
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
|
||||
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
|
||||
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
|
||||
|
||||
- `ChannelListCore` options property is removed in favor of individual properties
|
||||
- `options.state` -> bool state
|
||||
@@ -98,7 +124,7 @@ typedef MessageBuilder = Widget Function(
|
||||
);
|
||||
```
|
||||
|
||||
the last parameter is the default `MessageWidget`
|
||||
> **_NOTE:_** the last parameter is the default `MessageWidget`
|
||||
You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
|
||||
@@ -106,7 +132,8 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
- Added video compress options (frame and quality) to `MessageInput`
|
||||
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
|
||||
header/footer
|
||||
- `MessageWidget` accepts a `userAvatarBuilder`
|
||||
- Added pinMessage ui support
|
||||
- Added `MessageListView.threadSeparatorBuilder` property
|
||||
@@ -115,10 +142,12 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
|
||||
message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
|
||||
box when editing message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
|
||||
use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
|
||||
a reload
|
||||
- `MessageListView` not rendering if the user is not a member of the channel
|
||||
- Fix `MessageInput` overflow when there are no actions
|
||||
- Minor fixes and improvements
|
||||
@@ -128,17 +157,17 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
🛑️ Breaking Changes from `2.0.0-nullsafety.8`
|
||||
|
||||
- Renamed `ColorTheme` properties to reflect the purpose of the colors
|
||||
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
|
||||
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
|
||||
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
|
||||
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
|
||||
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
|
||||
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
|
||||
- `ColorTheme.white` -> `ColorTheme.barsBg`
|
||||
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
|
||||
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
|
||||
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
|
||||
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
|
||||
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
|
||||
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
|
||||
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
|
||||
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
|
||||
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
|
||||
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
|
||||
- `ColorTheme.white` -> `ColorTheme.barsBg`
|
||||
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
|
||||
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
|
||||
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
|
||||
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
|
||||
|
||||
✅ Added
|
||||
|
||||
@@ -165,21 +194,24 @@ typedef MessageBuilder = Widget Function(
|
||||
);
|
||||
```
|
||||
|
||||
the last parameter is the default `MessageWidget`
|
||||
You can call `.copyWith` to customize just a subset of properties
|
||||
> **_NOTE:_** The last parameter is the default `MessageWidget`
|
||||
You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
✅ Added
|
||||
|
||||
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
|
||||
header/footer
|
||||
- `MessageWidget` accepts a `userAvatarBuilder`
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
|
||||
message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
|
||||
box when editing message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
|
||||
use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
|
||||
a reload
|
||||
- `MessageListView` not rendering if the user is not a member of the channel
|
||||
|
||||
## 2.0.0-nullsafety.7
|
||||
@@ -249,7 +281,8 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
- Show error messages as system and keep them in the message input
|
||||
- Remove notification badge logic
|
||||
- Use shimmer while loading images
|
||||
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
|
||||
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated
|
||||
to `MessageInput`
|
||||
- Add possibility to specify custom message actions using `MessageWidget.customActions`
|
||||
- Added `MessageListView.onAttachmentTap` callback
|
||||
- Fixed message newline issue
|
||||
@@ -306,7 +339,8 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
- Improved api documentation
|
||||
- Updated `stream_chat` dependency to `^1.0.0-beta`
|
||||
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
|
||||
- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
|
||||
- Reimplemented existing widgets
|
||||
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
|
||||
|
||||
## 0.2.21
|
||||
|
||||
@@ -323,8 +357,8 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
## 0.2.20+2
|
||||
|
||||
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message
|
||||
arrives
|
||||
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the
|
||||
list when a new message arrives
|
||||
|
||||
## 0.2.20+1
|
||||
|
||||
@@ -358,7 +392,8 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
## 0.2.16
|
||||
|
||||
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation
|
||||
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress
|
||||
implementation
|
||||
- Make public autofocus field of the TextField of message_input
|
||||
|
||||
## 0.2.15
|
||||
@@ -543,10 +578,11 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
## 0.2.1-alpha+1
|
||||
|
||||
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget
|
||||
as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of
|
||||
your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to
|
||||
every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
|
||||
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have
|
||||
the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to
|
||||
your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in
|
||||
the usual way, but you need to add a `StreamChat` widget to every route of your app.
|
||||
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
|
||||
information.
|
||||
|
||||
```dart
|
||||
@@ -648,8 +684,8 @@ Widget build(BuildContext context) {
|
||||
|
||||
- Add gesture (vertical drag down) to close the keyboard
|
||||
|
||||
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the
|
||||
keyboard)
|
||||
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will
|
||||
even close the keyboard)
|
||||
|
||||
The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261
|
||||
|
||||
|
||||
@@ -584,11 +584,16 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
),
|
||||
],
|
||||
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
||||
ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
onImageTap: () => widget.onImageTap?.call(channel),
|
||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: chatThemeData.channelListViewTheme.backgroundColor,
|
||||
),
|
||||
child: ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
onImageTap: () => widget.onImageTap?.call(channel),
|
||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -94,13 +94,13 @@ class ChannelPreview extends StatelessWidget {
|
||||
textStyle: channelPreviewTheme.titleStyle,
|
||||
),
|
||||
),
|
||||
BetterStreamBuilder<List<Member>?>(
|
||||
BetterStreamBuilder<List<Member>>(
|
||||
stream: channel.state?.membersStream,
|
||||
initialData: channel.state?.members,
|
||||
comparator: const ListEquality().equals,
|
||||
builder: (context, members) {
|
||||
if (members?.isEmpty == true ||
|
||||
members?.any((Member e) =>
|
||||
if (members.isEmpty ||
|
||||
members.any((Member e) =>
|
||||
e.user!.id ==
|
||||
channel.client.state.currentUser?.id) !=
|
||||
true) {
|
||||
@@ -132,7 +132,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
message: lastMessage!,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: channel.state!.read
|
||||
?.where((element) =>
|
||||
.where((element) =>
|
||||
element.user.id !=
|
||||
channel
|
||||
.client.state.currentUser!.id)
|
||||
@@ -153,13 +153,10 @@ class ChannelPreview extends StatelessWidget {
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime?>(
|
||||
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime>(
|
||||
stream: channel.lastMessageAtStream,
|
||||
initialData: channel.lastMessageAt,
|
||||
builder: (context, data) {
|
||||
if (data == null) {
|
||||
return const Offstage();
|
||||
}
|
||||
final lastMessageAt = data.toLocal();
|
||||
|
||||
String stringDate;
|
||||
@@ -213,12 +210,12 @@ class ChannelPreview extends StatelessWidget {
|
||||
|
||||
Widget _buildLastMessage(BuildContext context) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BetterStreamBuilder<List<Message>?>(
|
||||
child: BetterStreamBuilder<List<Message>>(
|
||||
stream: channel.state!.messagesStream,
|
||||
initialData: channel.state!.messages,
|
||||
builder: (context, data) {
|
||||
final lastMessage = data
|
||||
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
||||
final lastMessage =
|
||||
data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted);
|
||||
if (lastMessage == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
||||
return BetterStreamBuilder<ConnectionStatus>(
|
||||
initialData: client.wsConnectionStatus,
|
||||
stream: stream,
|
||||
loadingBuilder: loadingBuilder,
|
||||
noDataBuilder: loadingBuilder,
|
||||
errorBuilder: (context, error) {
|
||||
if (errorBuilder != null) {
|
||||
return errorBuilder!(context, error);
|
||||
|
||||
@@ -46,9 +46,9 @@ extension PlatformFileX on PlatformFile {
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
/// Extension on [InputDecoration]
|
||||
extension InputDecorationX on InputDecoration {
|
||||
///
|
||||
/// Merges this [InputDecoration] with the [other]
|
||||
InputDecoration merge(InputDecoration? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
@@ -123,3 +123,50 @@ extension FlipBorder on BorderRadius {
|
||||
bottomRight: bottomLeft)
|
||||
: this;
|
||||
}
|
||||
|
||||
/// Extension on [IconButton]
|
||||
extension IconButtonX on IconButton {
|
||||
/// Creates a copy of [IconButton] with specified attributes overridden.
|
||||
IconButton copyWith({
|
||||
double? iconSize,
|
||||
VisualDensity? visualDensity,
|
||||
EdgeInsetsGeometry? padding,
|
||||
AlignmentGeometry? alignment,
|
||||
double? splashRadius,
|
||||
Color? color,
|
||||
Color? focusColor,
|
||||
Color? hoverColor,
|
||||
Color? highlightColor,
|
||||
Color? splashColor,
|
||||
Color? disabledColor,
|
||||
void Function()? onPressed,
|
||||
MouseCursor? mouseCursor,
|
||||
FocusNode? focusNode,
|
||||
bool? autofocus,
|
||||
String? tooltip,
|
||||
bool? enableFeedback,
|
||||
BoxConstraints? constraints,
|
||||
Widget? icon,
|
||||
}) =>
|
||||
IconButton(
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
visualDensity: visualDensity ?? this.visualDensity,
|
||||
padding: padding ?? this.padding,
|
||||
alignment: alignment ?? this.alignment,
|
||||
splashRadius: splashRadius ?? this.splashRadius,
|
||||
color: color ?? this.color,
|
||||
focusColor: focusColor ?? this.focusColor,
|
||||
hoverColor: hoverColor ?? this.hoverColor,
|
||||
highlightColor: highlightColor ?? this.highlightColor,
|
||||
splashColor: splashColor ?? this.splashColor,
|
||||
disabledColor: disabledColor ?? this.disabledColor,
|
||||
onPressed: onPressed ?? this.onPressed,
|
||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||
focusNode: focusNode ?? this.focusNode,
|
||||
autofocus: autofocus ?? this.autofocus,
|
||||
tooltip: tooltip ?? this.tooltip,
|
||||
enableFeedback: enableFeedback ?? this.enableFeedback,
|
||||
constraints: constraints ?? this.constraints,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,9 +142,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${widget.currentPage + 1} '
|
||||
'${context.translations.ofText} '
|
||||
'${widget.totalPages}',
|
||||
context.translations.galleryPaginationText(
|
||||
currentPage: widget.currentPage,
|
||||
totalPages: widget.totalPages),
|
||||
style: galleryFooterThemeData.titleTextStyle,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -100,6 +100,9 @@ abstract class Translations {
|
||||
/// The label for write a message in [MessageInput]
|
||||
String get writeAMessageLabel;
|
||||
|
||||
/// The label for slow mode enabled in [MessageInput]
|
||||
String get slowModeOnLabel;
|
||||
|
||||
/// The label for instant commands in [MessageInput]
|
||||
String get instantCommandsLabel;
|
||||
|
||||
@@ -296,8 +299,9 @@ abstract class Translations {
|
||||
/// The text shown for "You"
|
||||
String get youText;
|
||||
|
||||
/// The text shown for "Of"
|
||||
String get ofText;
|
||||
/// Gallery footer pagination text
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages});
|
||||
|
||||
/// The text shown for "File"
|
||||
String get fileText;
|
||||
@@ -659,7 +663,9 @@ class DefaultTranslations implements Translations {
|
||||
String get youText => 'You';
|
||||
|
||||
@override
|
||||
String get ofText => 'of';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} of $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'File';
|
||||
@@ -670,4 +676,7 @@ class DefaultTranslations implements Translations {
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) =>
|
||||
'Attachment limit exceeded, limit: $limit';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slow mode ON';
|
||||
}
|
||||
|
||||
@@ -60,6 +60,15 @@ typedef MentionTileBuilder = Widget Function(
|
||||
Member member,
|
||||
);
|
||||
|
||||
/// Widget builder for action button.
|
||||
///
|
||||
/// [defaultActionButton] is the default [IconButton] configuration,
|
||||
/// use [defaultActionButton.copyWith] to easily customize it.
|
||||
typedef ActionButtonBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
IconButton defaultActionButton,
|
||||
);
|
||||
|
||||
/// Location for actions on the [MessageInput]
|
||||
enum ActionsLocation {
|
||||
/// Align to left
|
||||
@@ -176,6 +185,8 @@ class MessageInput extends StatefulWidget {
|
||||
this.onError,
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceed,
|
||||
this.attachmentButtonBuilder,
|
||||
this.commandButtonBuilder,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -271,6 +282,18 @@ class MessageInput extends StatefulWidget {
|
||||
/// This will override the default error alert behaviour.
|
||||
final AttachmentLimitExceedListener? onAttachmentLimitExceed;
|
||||
|
||||
/// Builder for customizing the attachment button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? attachmentButtonBuilder;
|
||||
|
||||
/// Builder for customizing the command button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? commandButtonBuilder;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -316,10 +339,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty;
|
||||
late DateTime? _cooldownStartedAt;
|
||||
int? _timeOut;
|
||||
|
||||
Timer? _slowModeTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startSlowMode();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_emojiNames =
|
||||
Emoji.all().where((it) => it.name != null).map((e) => e.name!);
|
||||
@@ -350,6 +378,25 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
void _startSlowMode() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (channel.cooldownStartedAt != null) {
|
||||
_cooldownStartedAt = channel.cooldownStartedAt;
|
||||
if (DateTime.now().difference(_cooldownStartedAt!).inSeconds <
|
||||
channel.cooldown!) {
|
||||
_timeOut = channel.cooldown! -
|
||||
DateTime.now().difference(_cooldownStartedAt!).inSeconds;
|
||||
_slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_timeOut == 0) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() => _timeOut = _timeOut! - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = DecoratedBox(
|
||||
@@ -428,11 +475,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
children: <Widget>[
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.left)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
_buildTextInput(context),
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.right)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
@@ -498,24 +545,29 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
final sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
return AnimatedCrossFade(
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: sendButton,
|
||||
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||
duration: _messageInputTheme.sendAnimationDuration!,
|
||||
alignment: Alignment.center,
|
||||
late Widget sendButton;
|
||||
if (_timeOut != null && _timeOut! > 0) {
|
||||
sendButton = _CountdownButton(
|
||||
count: _timeOut!,
|
||||
);
|
||||
} else if (!_messageIsPresent && _attachments.isEmpty) {
|
||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandActionsButton() {
|
||||
Widget _buildExpandActionsButton(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
@@ -553,12 +605,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||
if (!widget.disableAttachments)
|
||||
_buildAttachmentButton(context),
|
||||
if (widget.showCommandsButton &&
|
||||
widget.editMessage == null &&
|
||||
channel.state != null &&
|
||||
channel.config?.commands.isNotEmpty == true)
|
||||
_buildCommandButton(),
|
||||
_buildCommandButton(context),
|
||||
...widget.actions ?? [],
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
),
|
||||
@@ -694,9 +747,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
: (widget.actionsLocation == ActionsLocation.leftInside
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildExpandActionsButton(),
|
||||
],
|
||||
children: [_buildExpandActionsButton(context)],
|
||||
)
|
||||
: null),
|
||||
suffixIconConstraints: const BoxConstraints.tightFor(height: 40),
|
||||
@@ -722,7 +773,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.rightInside)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
@@ -783,6 +834,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (_attachments.isNotEmpty) {
|
||||
return context.translations.addACommentOrSendLabel;
|
||||
}
|
||||
if (_timeOut != 0 && _timeOut != null) {
|
||||
return context.translations.slowModeOnLabel;
|
||||
}
|
||||
|
||||
return context.translations.writeAMessageLabel;
|
||||
}
|
||||
|
||||
@@ -1702,10 +1757,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCommandButton() {
|
||||
Widget _buildCommandButton(BuildContext context) {
|
||||
final s = _textEditingController.text.trim();
|
||||
|
||||
return IconButton(
|
||||
final defaultButton = IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: s.isNotEmpty
|
||||
? _streamChatTheme.colorTheme.disabled
|
||||
@@ -1743,38 +1797,46 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.commandButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
Widget _buildAttachmentButton() => IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? _messageInputTheme.actionButtonColor
|
||||
: _messageInputTheme.actionButtonIdleColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
Widget _buildAttachmentButton(BuildContext context) {
|
||||
final defaultButton = IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? _messageInputTheme.actionButtonColor
|
||||
: _messageInputTheme.actionButtonIdleColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.attachmentButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
/// Show the attachment modal, making the user choose where to
|
||||
/// pick a media from
|
||||
@@ -1904,15 +1966,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
} else if (fileType == DefaultAttachmentTypes.video) {
|
||||
pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera);
|
||||
}
|
||||
if (pickedFile == null) {
|
||||
return;
|
||||
if (pickedFile != null) {
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
}
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
} else {
|
||||
late FileType type;
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
@@ -2119,6 +2180,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (resp.message?.type == 'error') {
|
||||
_parseExistingMessage(message);
|
||||
}
|
||||
_startSlowMode();
|
||||
widget.onMessageSent?.call(resp.message);
|
||||
} catch (e, stk) {
|
||||
if (widget.onError != null) {
|
||||
@@ -2207,6 +2269,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
_emojiOverlay?.remove();
|
||||
_mentionsOverlay?.remove();
|
||||
_keyboardListener?.cancel();
|
||||
_slowModeTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -2332,3 +2395,30 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountdownButton extends StatelessWidget {
|
||||
const _CountdownButton({
|
||||
Key? key,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,8 +298,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
StreamChannelState? streamChannel;
|
||||
late StreamChatThemeData _streamTheme;
|
||||
|
||||
int? get _initialIndex {
|
||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||
int get _initialIndex {
|
||||
final initialScrollIndex = widget.initialScrollIndex;
|
||||
if (initialScrollIndex != null) return initialScrollIndex;
|
||||
if (streamChannel!.initialMessageId != null) {
|
||||
final messages = streamChannel!.channel.state!.messages;
|
||||
final totalMessages = messages.length;
|
||||
@@ -312,8 +313,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double? get _initialAlignment {
|
||||
if (widget.initialAlignment != null) return widget.initialAlignment;
|
||||
double get _initialAlignment {
|
||||
final initialAlignment = widget.initialAlignment;
|
||||
if (initialAlignment != null) return initialAlignment;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -326,8 +328,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int? initialIndex;
|
||||
double? initialAlignment;
|
||||
int initialIndex = 0;
|
||||
double initialAlignment = 0;
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
@@ -454,10 +456,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.separated(
|
||||
key: ValueKey(initialIndex! + initialAlignment!),
|
||||
key: _upToDate
|
||||
? null
|
||||
: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
initialScrollIndex: initialIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: widget.reverse,
|
||||
@@ -505,8 +509,14 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
if (i == 1 || i == itemCount - 4) return const Offstage();
|
||||
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = messages[i - 2];
|
||||
late final Message message, nextMessage;
|
||||
if (widget.reverse) {
|
||||
message = messages[i - 1];
|
||||
nextMessage = messages[i - 2];
|
||||
} else {
|
||||
message = messages[i - 2];
|
||||
nextMessage = messages[i - 1];
|
||||
}
|
||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
Units.DAY,
|
||||
@@ -636,8 +646,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
Positioned _buildFloatingDateDivider(int itemCount) => Positioned(
|
||||
top: widget.reverse ? 20 : null,
|
||||
bottom: widget.reverse ? null : 20,
|
||||
top: 20,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: BetterStreamBuilder<Iterable<ItemPosition>>(
|
||||
@@ -647,19 +656,36 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
final aTop = _getTopElementIndex(a);
|
||||
final bTop = _getTopElementIndex(b);
|
||||
return aTop == bTop;
|
||||
if (widget.reverse) {
|
||||
final aTop = _getTopElementIndex(a);
|
||||
final bTop = _getTopElementIndex(b);
|
||||
return aTop == bTop;
|
||||
} else {
|
||||
final aBottom = _getBottomElementIndex(a);
|
||||
final bBottom = _getBottomElementIndex(b);
|
||||
return aBottom == bBottom;
|
||||
}
|
||||
},
|
||||
builder: (context, values) {
|
||||
if (values.isEmpty || messages.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
final index = _getTopElementIndex(values);
|
||||
int? index;
|
||||
if (widget.reverse) {
|
||||
index = _getTopElementIndex(values);
|
||||
} else {
|
||||
index = _getBottomElementIndex(values);
|
||||
}
|
||||
|
||||
if (index == null || index <= 2 || index >= itemCount - 3) {
|
||||
return const Offstage();
|
||||
if (index == null) return const Offstage();
|
||||
|
||||
if (index <= 2 || index >= itemCount - 3) {
|
||||
if (widget.reverse) {
|
||||
index = itemCount - 4;
|
||||
} else {
|
||||
index = 2;
|
||||
}
|
||||
}
|
||||
|
||||
final message = messages[index - 2];
|
||||
@@ -685,6 +711,15 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
.index;
|
||||
}
|
||||
|
||||
int? _getBottomElementIndex(Iterable<ItemPosition> values) {
|
||||
final inView = values.where((position) => position.itemLeadingEdge < 1);
|
||||
if (inView.isEmpty) return null;
|
||||
return inView
|
||||
.reduce((min, position) =>
|
||||
position.itemLeadingEdge < min.itemLeadingEdge ? position : min)
|
||||
.index;
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
||||
@@ -814,7 +849,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
) {
|
||||
final isMyMessage =
|
||||
message.user!.id == StreamChat.of(context).currentUser!.id;
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
final currentUser = StreamChat.of(context).currentUser;
|
||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||
final currentUserMember =
|
||||
@@ -915,7 +950,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
final channel = streamChannel!.channel;
|
||||
final readList = channel.state?.read?.where((read) {
|
||||
final readList = channel.state?.read.where((read) {
|
||||
if (read.user.id == userId) return false;
|
||||
return read.lastRead.isAfter(message.createdAt) ||
|
||||
read.lastRead.isAtSameMomentAs(message.createdAt);
|
||||
@@ -953,7 +988,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final showInChannelIndicator = !_isThreadConversation && isThreadMessage;
|
||||
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||
@@ -1163,6 +1198,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
});
|
||||
|
||||
_messageNewListener =
|
||||
streamChannel!.channel.on(EventType.messageNew).listen((event) {
|
||||
if (_upToDate) {
|
||||
@@ -1203,8 +1245,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BetterStreamBuilder<Message>(
|
||||
stream: streamChannel!.channel.state!.messagesStream.map(
|
||||
(messages) =>
|
||||
messages!.firstWhere((m) => m.id == message.id)),
|
||||
(messages) => messages.firstWhere((m) => m.id == message.id)),
|
||||
initialData: message,
|
||||
builder: (_, data) => StreamChannel(
|
||||
channel: streamChannel!.channel,
|
||||
|
||||
@@ -1048,7 +1048,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: widget.message.text!.length > 200
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
@@ -1111,7 +1111,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: widget.message.text!.length > 200
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
@@ -1258,7 +1258,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
);
|
||||
|
||||
Widget _buildTextBubble() {
|
||||
if (widget.message.text!.trim().isEmpty) return const Offstage();
|
||||
if (widget.message.text?.trim().isEmpty ?? false) return const Offstage();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return IgnorePointer(
|
||||
child: BetterStreamBuilder<int?>(
|
||||
child: BetterStreamBuilder<int>(
|
||||
stream: cid != null
|
||||
? client.state.channels[cid]?.state?.unreadCountStream
|
||||
: client.state.totalUnreadCountStream,
|
||||
@@ -25,7 +25,7 @@ class UnreadIndicator extends StatelessWidget {
|
||||
? client.state.channels[cid]?.state?.unreadCount
|
||||
: client.state.totalUnreadCount,
|
||||
builder: (context, data) {
|
||||
if (data == null || data == 0) {
|
||||
if (data == 0) {
|
||||
return const Offstage();
|
||||
}
|
||||
return Material(
|
||||
|
||||
@@ -12,6 +12,7 @@ export 'src/channel_preview.dart';
|
||||
export 'src/connection_status_builder.dart';
|
||||
export 'src/date_divider.dart';
|
||||
export 'src/deleted_message.dart';
|
||||
export 'src/extension.dart' show IconButtonX;
|
||||
export 'src/full_screen_media.dart';
|
||||
export 'src/gallery_footer.dart';
|
||||
export 'src/gallery_header.dart';
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'it should show the the other member image',
|
||||
'it should show the other member image',
|
||||
(tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
@@ -74,9 +74,7 @@ void main() {
|
||||
userId: 'user-id2',
|
||||
user: User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage',
|
||||
},
|
||||
image: 'testimage',
|
||||
),
|
||||
)
|
||||
]));
|
||||
@@ -85,9 +83,7 @@ void main() {
|
||||
userId: 'user-id2',
|
||||
user: User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage',
|
||||
},
|
||||
image: 'testimage',
|
||||
),
|
||||
),
|
||||
Member(
|
||||
@@ -98,9 +94,7 @@ void main() {
|
||||
when(() => clientState.usersStream).thenAnswer((i) => Stream.value({
|
||||
'user-id2': User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage',
|
||||
},
|
||||
image: 'testimage',
|
||||
),
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
@@ -149,27 +143,21 @@ void main() {
|
||||
userId: 'user-id',
|
||||
user: User(
|
||||
id: 'user-id',
|
||||
extraData: const {
|
||||
'image': 'testimage1',
|
||||
},
|
||||
image: 'testimage1',
|
||||
),
|
||||
),
|
||||
Member(
|
||||
userId: 'user-id2',
|
||||
user: User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage2',
|
||||
},
|
||||
image: 'testimage2',
|
||||
),
|
||||
),
|
||||
Member(
|
||||
userId: 'user-id3',
|
||||
user: User(
|
||||
id: 'user-id3',
|
||||
extraData: const {
|
||||
'image': 'testimage3',
|
||||
},
|
||||
image: 'testimage3',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -21,6 +21,8 @@ void main() {
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAtStream)
|
||||
.thenAnswer((_) => Stream.value(lastMessageAt));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
|
||||
@@ -167,7 +167,7 @@ void main() {
|
||||
expect(translations.withText, isNotNull);
|
||||
expect(translations.inText, isNotNull);
|
||||
expect(translations.youText, isNotNull);
|
||||
expect(translations.ofText, isNotNull);
|
||||
expect(translations.galleryPaginationText, isNotNull);
|
||||
expect(translations.fileText, isNotNull);
|
||||
expect(translations.replyToMessageLabel, isNotNull);
|
||||
});
|
||||
|
||||
@@ -69,4 +69,69 @@ void main() {
|
||||
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'checks message input slow mode',
|
||||
(WidgetTester tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
final channelState = MockChannelState();
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.cooldown).thenReturn(10);
|
||||
when(() => channel.cooldownStartedAt).thenReturn(DateTime.now());
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
)
|
||||
]));
|
||||
when(() => channelState.members).thenReturn([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
),
|
||||
]);
|
||||
when(() => channelState.messages).thenReturn([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]);
|
||||
when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]));
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: const Scaffold(
|
||||
body: MessageInput(),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Slow mode ON'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user