Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into message-input-controller
This commit is contained in:
@@ -142,7 +142,9 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
context.translations.cancelLabel.toLowerCase(),
|
||||
context.translations.cancelLabel
|
||||
.toLowerCase()
|
||||
.capitalize(),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
|
||||
@@ -72,7 +72,11 @@ class ImageAttachment extends AttachmentWidget {
|
||||
}
|
||||
|
||||
var imageUri = Uri.parse(imageUrl);
|
||||
if (imageUri.host == 'stream-io-cdn.com') {
|
||||
if (imageUri.host.endsWith('stream-io-cdn.com') &&
|
||||
imageUri.queryParameters['h'] == '*' &&
|
||||
imageUri.queryParameters['w'] == '*' &&
|
||||
imageUri.queryParameters['crop'] == '*' &&
|
||||
imageUri.queryParameters['resize'] == '*') {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'h': '400',
|
||||
@@ -80,7 +84,7 @@ class ImageAttachment extends AttachmentWidget {
|
||||
'crop': 'center',
|
||||
'resize': 'crop',
|
||||
});
|
||||
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
|
||||
} else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'height': '400',
|
||||
@@ -93,7 +97,7 @@ class ImageAttachment extends AttachmentWidget {
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
CachedNetworkImage(
|
||||
cacheKey: imageUrl,
|
||||
cacheKey: imageUri.replace(queryParameters: {}).toString(),
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (context, __) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget.dart';
|
||||
@@ -145,7 +144,8 @@ class MessageListView extends StatefulWidget {
|
||||
this.threadBuilder,
|
||||
this.onThreadTap,
|
||||
this.dateDividerBuilder,
|
||||
this.scrollPhysics = const ClampingScrollPhysics(),
|
||||
this.scrollPhysics =
|
||||
const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
this.scrollController,
|
||||
@@ -225,7 +225,7 @@ class MessageListView extends StatefulWidget {
|
||||
final ItemPositionsListener? itemPositionListener;
|
||||
|
||||
/// The ScrollPhysics used by the ListView
|
||||
final ScrollPhysics scrollPhysics;
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// Called when message item gets swiped
|
||||
final OnMessageSwiped? onMessageSwiped;
|
||||
@@ -296,7 +296,7 @@ class MessageListView extends StatefulWidget {
|
||||
class _MessageListViewState extends State<MessageListView> {
|
||||
ItemScrollController? _scrollController;
|
||||
void Function(Message)? _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
|
||||
late final ItemPositionsListener _itemPositionListener;
|
||||
int? _messageListLength;
|
||||
StreamChannelState? streamChannel;
|
||||
@@ -306,12 +306,17 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final initialScrollIndex = widget.initialScrollIndex;
|
||||
if (initialScrollIndex != null) return initialScrollIndex;
|
||||
if (streamChannel!.initialMessageId != null) {
|
||||
final messages = streamChannel!.channel.state!.messages;
|
||||
final messages = streamChannel!.channel.state!.messages
|
||||
.where(widget.messageFilter ??
|
||||
defaultMessageFilter(
|
||||
streamChannel!.channel.client.state.currentUser!.id,
|
||||
))
|
||||
.toList(growable: false);
|
||||
final totalMessages = messages.length;
|
||||
final messageIndex =
|
||||
messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
|
||||
final index = totalMessages - messageIndex;
|
||||
if (index != 0) return index - 1;
|
||||
if (index != 0) return index + 1;
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
@@ -320,7 +325,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
double get _initialAlignment {
|
||||
final initialAlignment = widget.initialAlignment;
|
||||
if (initialAlignment != null) return initialAlignment;
|
||||
return 0;
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
||||
@@ -329,7 +334,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
bool get _isThreadConversation => widget.parentMessage != null;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int initialIndex = 0;
|
||||
@@ -337,6 +341,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
Map<String, int> messagesIndex = {};
|
||||
|
||||
bool initialMessageHighlightComplete = false;
|
||||
|
||||
bool _inBetweenList = false;
|
||||
@@ -382,6 +388,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
Widget _buildListView(List<Message> data) {
|
||||
messages = data;
|
||||
for (var index = 0; index < messages.length; index++) {
|
||||
messagesIndex[messages[index].id] = index;
|
||||
}
|
||||
final newMessagesListLength = messages.length;
|
||||
|
||||
if (_messageListLength != null) {
|
||||
@@ -390,14 +399,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength!;
|
||||
if (diff > 0) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
if (messages[0].user?.id !=
|
||||
streamChannel!.channel.client.state.currentUser?.id) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!_topPaginationActive && _upToDate) {
|
||||
// Reset the index in-case we send any new message
|
||||
initialIndex = 0;
|
||||
initialAlignment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +449,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
@@ -451,7 +458,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
@@ -462,17 +468,26 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.separated(
|
||||
key: _upToDate
|
||||
? null
|
||||
: ValueKey(initialIndex + initialAlignment),
|
||||
key: (initialIndex != 0 && initialAlignment != 0)
|
||||
? ValueKey('$initialIndex-$initialAlignment')
|
||||
: null,
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
initialScrollIndex: initialIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: widget.reverse,
|
||||
addAutomaticKeepAlives: false,
|
||||
itemCount: itemCount,
|
||||
findChildIndexCallback: (Key key) {
|
||||
final indexedKey = key as IndexedKey;
|
||||
final valueKey = indexedKey.key as ValueKey<String>?;
|
||||
if (valueKey != null) {
|
||||
final index = messagesIndex[valueKey.value];
|
||||
if (index != null) {
|
||||
return ((index + 2) * 2) - 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
|
||||
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)|
|
||||
@@ -624,14 +639,30 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i - 2);
|
||||
}
|
||||
return messageWidget;
|
||||
return KeyedSubtree(
|
||||
key: ValueKey(message.id),
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
BetterStreamBuilder<bool>(
|
||||
stream: streamChannel!.channel.state!.isUpToDateStream,
|
||||
initialData: streamChannel!.channel.state!.isUpToDate,
|
||||
builder: (context, snapshot) => ValueListenableBuilder<bool>(
|
||||
valueListenable: _showScrollToBottom,
|
||||
child: _buildScrollToBottom(),
|
||||
builder: (context, value, child) {
|
||||
if (!snapshot || value) {
|
||||
return child!;
|
||||
}
|
||||
return const Offstage();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.showFloatingDateDivider)
|
||||
_buildFloatingDateDivider(itemCount),
|
||||
],
|
||||
@@ -751,24 +782,15 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
.index;
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
||||
streamChannel!.channel.state!.unreadCountStream.distinct(),
|
||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||
),
|
||||
Widget _buildScrollToBottom() => StreamBuilder<int>(
|
||||
stream: streamChannel!.channel.state!.unreadCountStream,
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Offstage();
|
||||
} else if (!snapshot.hasData) {
|
||||
return const Offstage();
|
||||
}
|
||||
final isUpToDate = snapshot.data!.item1;
|
||||
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
||||
if (!showScrollToBottom) {
|
||||
return const Offstage();
|
||||
}
|
||||
final unreadCount = snapshot.data!.item2;
|
||||
final unreadCount = snapshot.data!;
|
||||
final showUnreadCount = unreadCount > 0 &&
|
||||
streamChannel!.channel.state!.members.any((e) =>
|
||||
e.userId ==
|
||||
@@ -783,16 +805,21 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: _streamTheme.colorTheme.barsBg,
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel!.channel.markRead();
|
||||
}
|
||||
if (!_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
streamChannel!.reloadChannel();
|
||||
initialAlignment = 0;
|
||||
initialIndex = 0;
|
||||
await streamChannel!.reloadChannel();
|
||||
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||
_scrollController!.jumpTo(index: 0);
|
||||
});
|
||||
} else {
|
||||
setState(() => _showScrollToBottom = false);
|
||||
_showScrollToBottom.value = false;
|
||||
_scrollController!.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(seconds: 1),
|
||||
@@ -854,9 +881,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
int index,
|
||||
) {
|
||||
final messageWidget = buildMessage(message, messages, index);
|
||||
|
||||
return VisibilityDetector(
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'),
|
||||
key: ValueKey('visibility: ${message.id}'),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (isVisible) {
|
||||
@@ -868,8 +894,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
if (_showScrollToBottom == isVisible) {
|
||||
setState(() => _showScrollToBottom = !isVisible);
|
||||
if (_showScrollToBottom.value == isVisible) {
|
||||
_showScrollToBottom.value = !isVisible;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -948,16 +974,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return defaultMessageWidget;
|
||||
}
|
||||
|
||||
Widget buildMessage(
|
||||
Message message,
|
||||
List<Message> messages,
|
||||
int index,
|
||||
) {
|
||||
Widget buildMessage(Message message, List<Message> messages, int index) {
|
||||
if ((message.type == 'system' || message.type == 'error') &&
|
||||
message.text?.isNotEmpty == true) {
|
||||
return widget.systemMessageBuilder?.call(context, message) ??
|
||||
SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onSystemMessageTap != null) {
|
||||
@@ -1037,7 +1058,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
|
||||
Widget messageWidget = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
@@ -1049,23 +1069,20 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
showSendingIndicator: showSendingIndicator,
|
||||
showUserAvatar: showUserAvatar,
|
||||
onQuotedMessageTap: (quotedMessageId) async {
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
final scrollToIndex = () {
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||
_scrollController?.scrollTo(
|
||||
index: index,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
index: index + 2, // +2 to account for loader and footer
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: 0.1,
|
||||
);
|
||||
};
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
} else {
|
||||
await streamChannel!.loadChannelAtMessage(quotedMessageId).then((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
}
|
||||
});
|
||||
await streamChannel!
|
||||
.loadChannelAtMessage(quotedMessageId)
|
||||
.then((_) async {
|
||||
initialIndex = 21; // 19 + 2 | 19 is the index of the message
|
||||
initialAlignment = 0.1;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1198,7 +1215,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: child,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
@@ -1229,27 +1246,25 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
if (_scrollController?.isAttached == true) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (_scrollController?.isAttached == true) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
_messageNewListener =
|
||||
streamChannel!.channel.on(EventType.messageNew).listen((event) {
|
||||
if (_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
}
|
||||
if (event.message?.parentId == widget.parentMessage?.id &&
|
||||
event.message!.user!.id ==
|
||||
streamChannel!.channel.client.state.currentUser!.id) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_scrollController?.jumpTo(
|
||||
_scrollController?.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,6 +608,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
final bottomRowPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||
|
||||
final showReactions = _shouldShowReactions;
|
||||
|
||||
return Material(
|
||||
type: widget.message.pinned && widget.showPinHighlight
|
||||
? MaterialType.card
|
||||
@@ -671,17 +673,23 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
SizedBox(width: avatarWidth + 4),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform: Matrix4.translationValues(
|
||||
widget.reverse ? 12 : -12,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 22 * 6.0,
|
||||
),
|
||||
child: _buildReactionIndicator(context),
|
||||
),
|
||||
visible: showReactions,
|
||||
portal: showReactions
|
||||
? Container(
|
||||
transform:
|
||||
Matrix4.translationValues(
|
||||
widget.reverse ? 12 : -12,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 22 * 6.0,
|
||||
),
|
||||
child: _buildReactionIndicator(
|
||||
context,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
portalAnchor:
|
||||
Alignment(widget.reverse ? 1 : -1, -1),
|
||||
childAnchor:
|
||||
@@ -1036,9 +1044,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: (widget.showReactions &&
|
||||
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
||||
!widget.message.isDeleted)
|
||||
child: _shouldShowReactions
|
||||
? GestureDetector(
|
||||
onTap: () => _showMessageReactionsModalBottomSheet(context),
|
||||
child: ReactionBubble(
|
||||
@@ -1058,6 +1064,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
);
|
||||
}
|
||||
|
||||
bool get _shouldShowReactions =>
|
||||
widget.showReactions &&
|
||||
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
||||
!widget.message.isDeleted;
|
||||
|
||||
void _showMessageActionModalBottomSheet(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user