Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into feat/capabilities

 Conflicts:
	packages/stream_chat/CHANGELOG.md
	packages/stream_chat_flutter/CHANGELOG.md
This commit is contained in:
Deven Joshi
2021-12-03 15:24:09 +05:30
56 changed files with 744 additions and 376 deletions
+22 -4
View File
@@ -4,18 +4,36 @@
- `pinPermissions` is no longer needed in `MessageListView`.
## 3.3.1
✅ Added
- `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`.
- `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call.
- Added `MessageListView.keyboardDismissBehavior` property.
🐞 Fixed
- [[#766]]`AttachmentActionsModal` now has customisation options for actions.
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
- Fixed adding attachments on web.
- [[#767]](https://github.com/GetStream/stream-chat-flutter/issues/767): Fix `MessageInput` focus behaviour when sending messages.
- Fixed user presence indicator not updating correctly.
- Do not use `withData: true` in `FilePicker` calls.
- Fixed read indicator not updating correctly in specific situations.
## 3.2.0
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`.
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🐞 Fixed
- Fixed message highlight animation alignment in `MessageListView`
- Fixed message highlight animation alignment in `MessageListView`.
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
- Fixed `MessageListView` initialIndex not working in some cases.
- Improved `MessageListView` rendering in case of reordering.
- Fix image thumbnail generation when using Stream CDN
- Fix image thumbnail generation when using Stream CDN.
✅ Added
@@ -866,4 +884,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
## 0.0.1
- First release
- First release
@@ -64,11 +64,12 @@ class MyApp extends StatelessWidget {
),
),
messageListViewTheme: const MessageListViewThemeData(
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
)),
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
),
),
otherMessageTheme: MessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle(
@@ -45,6 +45,7 @@ class PositionedList extends StatefulWidget {
this.addSemanticIndexes = true,
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
this.keyboardDismissBehavior,
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
'positionedIndex cannot be 0 and must be smaller than itemCount'),
super(key: key);
@@ -134,6 +135,10 @@ class PositionedList extends StatefulWidget {
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
final bool addAutomaticKeepAlives;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
@override
State<StatefulWidget> createState() => _PositionedListState();
}
@@ -173,6 +178,7 @@ class _PositionedListState extends State<PositionedList> {
anchor: widget.alignment,
center: _centerKey,
controller: scrollController,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: widget.cacheExtent,
@@ -28,9 +28,12 @@ class UnboundedCustomScrollView extends CustomScrollView {
List<Widget> slivers = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
}) : _anchor = anchor,
super(
key: key,
keyboardDismissBehavior: keyboardDismissBehavior ??
ScrollViewKeyboardDismissBehavior.manual,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
@@ -52,6 +52,7 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
separatorBuilder = null,
super(key: key);
@@ -77,6 +78,7 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
super(key: key);
@@ -92,6 +94,10 @@ class ScrollablePositionedList extends StatefulWidget {
/// index of the child element with that associated key, or null if not found.
final ChildIndexGetter? findChildIndexCallback;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
/// Number of items the [itemBuilder] can produce.
final int itemCount;
@@ -344,6 +350,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
child: NotificationListener<ScrollNotification>(
onNotification: (_) => _isTransitioning,
child: PositionedList(
keyboardDismissBehavior: widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
@@ -374,6 +381,8 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
child: NotificationListener<ScrollNotification>(
onNotification: (_) => false,
child: PositionedList(
keyboardDismissBehavior:
widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
@@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget {
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
this.showReply = true,
this.showShowInChat = true,
this.showSave = true,
this.showDelete = true,
this.customActions = const [],
}) : super(key: key);
/// The message containing the attachments
@@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to provide download files
final AttachmentDownloader? fileDownloader;
/// Show reply option
final bool showReply;
/// Show show in chat option
final bool showShowInChat;
/// Show save option
final bool showSave;
/// Show delete option
final bool showDelete;
/// List of custom actions
final List<AttachmentAction> customActions;
/// Creates a copy of [MessageWidget] with specified attributes overridden.
AttachmentActionsModal copyWith({
Key? key,
int? currentIndex,
Message? message,
VoidCallback? onShowMessage,
AttachmentDownloader? imageDownloader,
AttachmentDownloader? fileDownloader,
bool? showReply,
bool? showShowInChat,
bool? showSave,
bool? showDelete,
List<AttachmentAction>? customActions,
}) =>
AttachmentActionsModal(
key: key ?? this.key,
currentIndex: currentIndex ?? this.currentIndex,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
imageDownloader: imageDownloader ?? this.imageDownloader,
fileDownloader: fileDownloader ?? this.fileDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
showSave: showSave ?? this.showSave,
showDelete: showDelete ?? this.showDelete,
customActions: customActions ?? this.customActions,
);
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
@@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
context,
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
if (showReply)
_buildButton(
context,
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
),
_buildButton(
context,
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
if (showShowInChat)
_buildButton(
context,
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
),
onShowMessage,
),
onShowMessage,
),
_buildButton(
context,
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
if (showSave)
_buildButton(
context,
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image';
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
downloader(
attachment,
progressCallback: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
).catchError((e, stk) {
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
barrierColor: theme.colorTheme.overlay,
builder: (context) => _buildDownloadProgressDialog(
context,
progressNotifier,
),
);
},
),
() {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image';
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
downloader(
attachment,
progressCallback: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
).catchError((e, stk) {
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
barrierColor: theme.colorTheme.overlay,
builder: (context) => _buildDownloadProgressDialog(
context,
progressNotifier,
),
);
},
),
if (StreamChat.of(context).currentUser?.id ==
message.user?.id)
message.user?.id &&
showDelete)
_buildButton(
context,
context.translations.deleteLabel.capitalize(),
@@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget {
},
color: theme.colorTheme.accentError,
),
...customActions
.map(
(e) => _buildButton(
context,
e.actionTitle,
e.icon,
e.onTap,
),
)
.toList(),
]
.map<Widget>((e) => Align(
alignment: Alignment.centerRight,
@@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget {
Widget _buildButton(
context,
String title,
StreamSvgIcon icon,
Widget icon,
VoidCallback? onTap, {
Color? color,
Key? key,
@@ -331,3 +393,22 @@ class _DownloadProgress {
int get toPercentage => (received * 100) ~/ total;
}
/// Class for custom attachment action
class AttachmentAction {
/// Constructor for custom attachment action
AttachmentAction({
required this.actionTitle,
required this.icon,
required this.onTap,
});
/// Title for the attachment action
String actionTitle;
/// Icon for the attachment action
Widget icon;
/// Callback for when the action is tapped
VoidCallback onTap;
}
@@ -59,9 +59,10 @@ class ChannelInfo extends StatelessWidget {
final memberCount = channel.memberCount;
if (memberCount != null && memberCount > 2) {
var text = context.translations.membersCountText(memberCount);
final watcherCount = channel.state?.watcherCount ?? 0;
if (watcherCount > 0) {
text += ' ${context.translations.watchersCountText(watcherCount)}';
final onlineCount =
members?.where((m) => m.user?.online == true).length ?? 0;
if (onlineCount > 0) {
text += ', ${context.translations.watchersCountText(onlineCount)}';
}
alternativeWidget = Text(
text,
@@ -126,16 +126,26 @@ class ChannelPreview extends StatelessWidget {
streamChatState.currentUser?.id) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state!.read
.where((element) =>
element.user.id !=
channel.client.state.currentUser!.id)
.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
.isNotEmpty,
child: BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
builder: (context, data) {
final readList = data.where((it) =>
it.user.id !=
channel.client.state.currentUser?.id &&
(it.lastRead
.isAfter(lastMessage!.createdAt) ||
it.lastRead.isAtSameMomentAs(
lastMessage.createdAt,
)));
final isMessageRead = readList.length >=
(channel.memberCount ?? 0) - 1;
return SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: isMessageRead,
);
},
),
);
}
@@ -1,6 +1,7 @@
import 'package:characters/characters.dart';
import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/localization/translations.dart';
@@ -46,7 +47,7 @@ extension IterableX<T> on Iterable<T> {
extension PlatformFileX on PlatformFile {
/// Converts the [PlatformFile] into [AttachmentFile]
AttachmentFile get toAttachmentFile => AttachmentFile(
path: path,
path: kIsWeb ? null : path,
name: name,
bytes: bytes,
size: size,
@@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget {
this.startIndex = 0,
String? userName,
this.onShowMessage,
this.attachmentActionsModalBuilder,
}) : userName = userName ?? '',
super(key: key);
@@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget {
/// Callback for when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
}
@@ -196,6 +202,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
StreamChannel.of(context).channel,
);
},
attachmentActionsModalBuilder:
widget.attachmentActionsModalBuilder,
),
if (!widget.message.isEphemeral)
GalleryFooter(
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
@@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
typedef AttachmentActionsBuilder = Widget Function(
BuildContext context,
Attachment attachment,
AttachmentActionsModal defaultActionsModal,
);
/// Header/AppBar widget for media display screen
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
@@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
this.userName = '',
this.sentAt = '',
this.backgroundColor,
this.attachmentActionsModalBuilder,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color of this [GalleryHeader].
final Color? backgroundColor;
/// Widget builder for attachment actions modal
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
/// Use [defaultActionsModal.copyWith] to easily customize it
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
@override
Widget build(BuildContext context) {
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
@@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
final galleryHeaderThemeData =
StreamChatTheme.of(context).galleryHeaderTheme;
final defaultModal = AttachmentActionsModal(
message: message,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
);
final effectiveModal = attachmentActionsModalBuilder?.call(
context,
message.attachments[currentIndex],
defaultModal,
) ??
defaultModal;
final result = await showDialog(
useRootNavigator: false,
context: context,
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
builder: (context) => StreamChannel(
channel: channel,
child: AttachmentActionsModal(
message: message,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
),
child: effectiveModal,
),
);
@@ -81,6 +81,7 @@ class GroupAvatar extends StatelessWidget {
),
initialData: member,
builder: (context, member) => UserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
),
@@ -118,6 +119,7 @@ class GroupAvatar extends StatelessWidget {
),
initialData: member,
builder: (context, member) => UserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
),
@@ -204,6 +204,7 @@ class MessageInput extends StatefulWidget {
this.commandButtonBuilder,
this.customOverlays = const [],
this.mentionAllAppUsers = false,
this.shouldKeepFocusAfterMessage,
}) : assert(
initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`",
@@ -322,6 +323,10 @@ class MessageInput extends StatefulWidget {
/// Defaults to false.
final bool mentionAllAppUsers;
/// Defines if the [MessageInput] loses focuses after a message is sent.
/// The default behaviour keeps focus until a command is enabled.
final bool? shouldKeepFocusAfterMessage;
@override
MessageInputState createState() => MessageInputState();
@@ -1163,7 +1168,9 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildMentionsOverlayEntry() {
if (textEditingController.value.selection.start < 0) {
final channel = StreamChannel.of(context).channel;
if (textEditingController.value.selection.start < 0 ||
channel.state == null) {
return const Offstage();
}
@@ -1192,7 +1199,7 @@ class MessageInputState extends State<MessageInput> {
query: query,
mentionAllAppUsers: widget.mentionAllAppUsers,
client: StreamChat.of(context).client,
channel: StreamChannel.of(context).channel,
channel: channel,
size: Size(renderObject.size.width - 16, 400),
mentionsTileBuilder: tileBuilder,
onMentionUserTap: (user) {
@@ -1640,7 +1647,6 @@ class MessageInputState extends State<MessageInput> {
}
final res = await FilePicker.platform.pickFiles(
type: type,
withData: true,
);
if (res?.files.isNotEmpty == true) {
file = res!.files.single.toAttachmentFile;
@@ -1759,7 +1765,9 @@ class MessageInputState extends State<MessageInput> {
return;
}
final shouldUnfocus = _commandEnabled;
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled;
if (_commandEnabled) {
text = '${'/${_chosenCommand!.name} '}$text';
@@ -1822,8 +1830,10 @@ class MessageInputState extends State<MessageInput> {
sendingFuture = channel.updateMessage(message);
}
if (!shouldUnfocus) {
if (shouldKeepFocus) {
FocusScope.of(context).requestFocus(_focusNode);
} else {
FocusScope.of(context).unfocus();
}
final resp = await sendingFuture;
@@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message);
/// Callback on reply tapped
typedef ReplyTapCallback = void Function(Message);
/// Spacing Types (These are properties of a message to help inform the decision
/// of how much space / which widget to build after it)
enum SpacingType {
/// Message is a thread
thread,
/// There is a >1s time diff between current and last message
timeDiff,
/// Next message is by a different user
otherUser,
/// Message is deleted
deleted,
/// No other conditions are valid, default spacing (This will likely be the
/// only rule in the list provided)
defaultSpacing,
}
/// Builder for building certain spacing after widgets.
/// This spacing can be in form of any widgets you like.
/// A List of [SpacingType] is provided to help inform the decision of
/// what to build after the message.
///
/// As an example:
/// MessageListView(
/// spacingWidgetBuilder: (context, list) {
/// if(list.contains(SpacingType.defaultSpacing)) {
/// return SizedBox(height: 2.0,);
/// } else {
/// return SizedBox(height: 8.0,);
/// }
/// },
/// ),
typedef SpacingWidgetBuilder = Widget Function(
BuildContext context,
List<SpacingType> spacingTypes,
);
/// Class for message details
// ignore: prefer-match-file-name
class MessageDetails {
@@ -170,8 +210,14 @@ class MessageListView extends StatefulWidget {
this.reverse = true,
this.paginationLimit = 20,
this.paginationLoadingIndicatorBuilder,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
this.spacingWidgetBuilder,
}) : super(key: key);
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// Function used to build a custom message widget
final MessageBuilder? messageBuilder;
@@ -285,6 +331,12 @@ class MessageListView extends StatefulWidget {
/// Builder used to build the loading indicator shown while paginating.
final WidgetBuilder? paginationLoadingIndicatorBuilder;
/// This allows a user to customise the space after a message
/// A List of [SpacingType] is provided to provide more data about the
/// type of message (thread, difference in time between current and last
/// message, default spacing, etc)
final SpacingWidgetBuilder? spacingWidgetBuilder;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -440,9 +492,6 @@ class _MessageListViewState extends State<MessageListView> {
childAnchor: Alignment.topCenter,
message: statusString,
child: LazyLoadScrollView(
onPageScrollStart: () {
FocusScope.of(context).unfocus();
},
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
@@ -468,6 +517,7 @@ class _MessageListViewState extends State<MessageListView> {
key: (initialIndex != 0 && initialAlignment != 0)
? ValueKey('$initialIndex-$initialAlignment')
: null,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
itemPositionsListener: _itemPositionListener,
initialScrollIndex: initialIndex,
initialAlignment: initialAlignment,
@@ -561,17 +611,38 @@ class _MessageListViewState extends State<MessageListView> {
Units.MINUTE,
);
final spacingRules = <SpacingType>[];
final isNextUserSame =
message.user!.id == nextMessage.user?.id;
final isThread = message.replyCount! > 0;
final isDeleted = message.isDeleted;
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return const SizedBox(height: 8);
final hasTimeDiff = timeDiff >= 1;
if (hasTimeDiff) {
spacingRules.add(SpacingType.timeDiff);
}
return const SizedBox(height: 2);
if (!isNextUserSame) {
spacingRules.add(SpacingType.otherUser);
}
if (isThread) {
spacingRules.add(SpacingType.thread);
}
if (isDeleted) {
spacingRules.add(SpacingType.deleted);
}
if (spacingRules.isNotEmpty) {
return widget.spacingWidgetBuilder
?.call(context, spacingRules) ??
const SizedBox(height: 8);
}
return widget.spacingWidgetBuilder
?.call(context, [SpacingType.defaultSpacing]) ??
const SizedBox(height: 2);
},
itemBuilder: (context, i) {
if (i == itemCount - 1) {
@@ -1000,15 +1071,6 @@ class _MessageListViewState extends State<MessageListView> {
);
}
final channel = streamChannel!.channel;
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);
}).toList() ??
[];
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final hasFileAttachment =
message.attachments.any((it) => it.type == 'file');
@@ -1137,8 +1199,6 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage
? _streamTheme.ownMessageTheme
: _streamTheme.otherMessageTheme,
readList: readList,
allRead: allRead,
onReturnAction: (action) {
switch (action) {
case ReturnActionType.none:
@@ -97,14 +97,20 @@ class MessageWidget extends StatefulWidget {
this.deletedBottomRowBuilder,
this.onReturnAction,
this.customAttachmentBuilders,
this.readList,
this.padding,
this.textPadding = const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
this.attachmentPadding = EdgeInsets.zero,
this.allRead = false,
@Deprecated('''
allRead is now deprecated and it will be removed in future releases.
The MessageWidget now listens for read events on its own.
''') this.allRead = false,
@Deprecated('''
readList is now deprecated and it will be removed in future releases.
The MessageWidget now listens for read events on its own.
''') this.readList,
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
@@ -508,7 +514,6 @@ class MessageWidget extends StatefulWidget {
showUserAvatar: showUserAvatar ?? this.showUserAvatar,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
showReactions: showReactions ?? this.showReactions,
allRead: allRead ?? this.allRead,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannelIndicator:
@@ -517,7 +522,6 @@ class MessageWidget extends StatefulWidget {
onLinkTap: onLinkTap ?? this.onLinkTap,
showReactionPickerIndicator:
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
readList: readList ?? this.readList,
onShowMessage: onShowMessage ?? this.onShowMessage,
onReturnAction: onReturnAction ?? this.onReturnAction,
showUsername: showUsername ?? this.showUsername,
@@ -558,8 +562,6 @@ class _MessageWidgetState extends State<MessageWidget>
bool get showTimeStamp => widget.showTimestamp;
bool get isMessageRead => widget.readList?.isNotEmpty == true;
bool get showInChannel => widget.showInChannelIndicator;
bool get hasQuotedMessage => widget.message.quotedMessage != null;
@@ -1223,6 +1225,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildSendingIndicator() {
final style = widget.messageTheme.createdAtStyle;
final message = widget.message;
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
if (hasNonUrlAttachments &&
(message.status == MessageSendingStatus.sending ||
@@ -1245,27 +1248,40 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
Widget child = SendingIndicator(
message: message,
isMessageRead: isMessageRead,
size: style!.fontSize,
final channel = StreamChannel.of(context).channel;
return BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
builder: (context, data) {
final readList = data.where((it) =>
it.user.id != _streamChat.currentUser?.id &&
(it.lastRead.isAfter(message.createdAt) ||
it.lastRead.isAtSameMomentAs(message.createdAt)));
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
Widget child = SendingIndicator(
message: message,
isMessageRead: isMessageRead,
size: style!.fontSize,
);
if (isMessageRead) {
child = Row(
children: [
if (memberCount > 2)
Text(
readList.length.toString(),
style: style.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
const SizedBox(width: 2),
child,
],
);
}
return child;
},
);
if (isMessageRead) {
child = Row(
children: [
if (StreamChannel.of(context).channel.memberCount! > 2)
Text(
widget.readList!.length.toString(),
style: style.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
const SizedBox(width: 2),
child,
],
);
}
return child;
}
Widget _buildUserAvatar() => Transform.translate(
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
export 'src/attachment_actions_modal.dart';
export 'src/back_button.dart';
export 'src/channel_avatar.dart';
export 'src/channel_header.dart';
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 3.2.0
version: 3.3.1
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -36,7 +36,7 @@ dependencies:
rxdart: ^0.27.0
share_plus: ^3.0.4
shimmer: ^2.0.0
stream_chat_flutter_core: ^3.2.0
stream_chat_flutter_core: ^3.3.0
substring_highlight: ^1.0.26
synchronized: ^3.0.0
url_launcher: ^6.0.3