Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 39 additions & 32 deletions lib/src/widgets/chat_groupedlist_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import 'package:chatview_utils/chatview_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

import '../extensions/extensions.dart';
import '../models/config_models/feature_active_config.dart';
Expand Down Expand Up @@ -54,7 +55,7 @@ class ChatGroupedListWidget extends StatefulWidget {
final bool showPopUp;

/// Pass scroll controller
final ScrollController scrollController;
final AutoScrollController scrollController;

/// Provides callback for assigning reply message when user swipe on chat bubble.
final ValueSetter<Message> assignReplyMessage;
Expand Down Expand Up @@ -214,25 +215,14 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
// The message is in the list but not rendered yet.
// Scroll slightly repeatedly to ensure it is rendered.
if (repliedMsgState == null) {
// Calculate total scroll extent and visible portion
final controllerPosition = widget.scrollController.position;

// Calculate a target position based on relative index position
// This estimates where the message might be in the list
final scrollExtent = controllerPosition.maxScrollExtent;
final targetPosition = scrollExtent * ((index + 1) / messages.length);

// Start a bit before the estimated position to avoid overshooting
final visibleHeight = controllerPosition.viewportDimension;
final scrollPosition = targetPosition - (visibleHeight * 0.85);

widget.scrollController
.animateTo(
scrollPosition,
curve: Curves.ease,
duration: const Duration(milliseconds: 50),
)
.then((_) => _onReplyTap(id, messages, messageIndex: index));
// Use scrollToIndex for more reliable scrolling
await widget.scrollController.scrollToIndex(
index,
preferPosition: AutoScrollPosition.middle,
duration: const Duration(milliseconds: 300),
);
// Try again after scrolling
_onReplyTap(id, messages, messageIndex: index);
return;
}

Expand Down Expand Up @@ -348,22 +338,32 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
// Since the list is reversed, check if it's the last item
// to display the loading widget at top.
if (_isPrevPageLoading.value && index == itemCount - 1) {
return PaginationLoader(
listenable: _isPrevPageLoading,
loader: widget.loadingWidget,
return AutoScrollTag(
key: ValueKey('loading_$index'),
controller: widget.scrollController,
index: index,
child: PaginationLoader(
listenable: _isPrevPageLoading,
loader: widget.loadingWidget,
),
);
}

/// Check [messageSeparator] contains group separator for [index]
if (enableSeparator && messageSeparator.containsKey(index)) {
final separator = messageSeparator[index]!;
return chatBackgroundConfig.groupSeparatorBuilder
?.call(separator.toString()) ??
ChatGroupHeader(
day: separator,
groupSeparatorConfig:
chatBackgroundConfig.defaultGroupSeparatorConfig,
);
return AutoScrollTag(
key: ValueKey('separator_$index'),
controller: widget.scrollController,
index: index,
child: chatBackgroundConfig.groupSeparatorBuilder
?.call(separator.toString()) ??
ChatGroupHeader(
day: separator,
groupSeparatorConfig: chatBackgroundConfig
.defaultGroupSeparatorConfig,
),
);
}

/// By removing separators encountered till now from the [index]
Expand Down Expand Up @@ -400,16 +400,23 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
},
);

final autoScrollChild = AutoScrollTag(
key: ValueKey('message_$index'),
controller: widget.scrollController,
index: index,
child: messageChild,
);

return index != 0
? messageChild
? autoScrollChild
// Since the list is reversed, we need to check if
// we are at the first item to display the typing indicator
// , suggestions and loading widget.
: EndMessageFooter(
loadingWidget: widget.loadingWidget,
isNextPageLoading: _isNextPageLoading,
typingIndicatorNotifier: typingIndicatorNotifier,
child: messageChild,
child: autoScrollChild,
);
},
),
Expand Down
46 changes: 41 additions & 5 deletions lib/src/widgets/chat_list_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'dart:io' if (kIsWeb) 'dart:html';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

import '../../chatview.dart';
import '../extensions/extensions.dart';
Expand Down Expand Up @@ -65,15 +66,17 @@ class ChatListWidget extends StatefulWidget {
final TextFieldConfiguration? textFieldConfig;

@override
State<ChatListWidget> createState() => _ChatListWidgetState();
State<ChatListWidget> createState() => ChatListWidgetState();
}

class _ChatListWidgetState extends State<ChatListWidget> {
class ChatListWidgetState extends State<ChatListWidget> {
ChatController get chatController => widget.chatController;

List<Message> get messageList => chatController.initialMessageList;

ScrollController get scrollController => chatController.scrollController;
/// 使用 chatController.scrollController(由外部传入的 AutoScrollController)
AutoScrollController get autoScrollController =>
chatController.scrollController as AutoScrollController;

FeatureActiveConfig? featureActiveConfig;
ChatUser? currentUser;
Expand Down Expand Up @@ -105,7 +108,7 @@ class _ChatListWidgetState extends State<ChatListWidget> {
loadMoreData: widget.loadMoreData,
loadingWidget: widget.loadingWidget,
showPopUp: showPopupValue,
scrollController: scrollController,
scrollController: autoScrollController,
isEnableSwipeToSeeTime:
featureActiveConfig?.enableSwipeToSeeTime ?? true,
assignReplyMessage: widget.assignReplyMessage,
Expand Down Expand Up @@ -137,10 +140,43 @@ class _ChatListWidgetState extends State<ChatListWidget> {
if (!chatController.messageStreamController.isClosed) {
chatController.messageStreamController.add(messageList);
}
if (messageList.isNotEmpty) chatController.scrollToLastMessage();
if (messageList.isNotEmpty) _scrollToLastMessage();
});
}

/// Scroll to last message using AutoScrollController
void _scrollToLastMessage() {
if (messageList.isNotEmpty) {
// Scroll to the first index (which is the last message due to reverse: true)
autoScrollController.scrollToIndex(
0,
preferPosition: AutoScrollPosition.begin,
duration: const Duration(milliseconds: 300),
);
}
}

/// Public method to scroll to a specific message index
Future<void> scrollToIndex(int index,
{AutoScrollPosition? preferPosition}) async {
await autoScrollController.scrollToIndex(
index,
preferPosition: preferPosition ?? AutoScrollPosition.begin,
duration: const Duration(milliseconds: 300),
);
}

/// Scroll to a specific message by its ID
Future<void> scrollToMessageById(String messageId) async {
final index = messageList.indexWhere((message) => message.id == messageId);
if (index != -1) {
// Convert to list view index (list is reversed)
final listViewIndex = messageList.length - 1 - index;
await scrollToIndex(listViewIndex,
preferPosition: AutoScrollPosition.middle);
}
}

void _showReplyPopup({
required Message message,
required bool sentByCurrentUser,
Expand Down
57 changes: 57 additions & 0 deletions lib/src/widgets/chat_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'package:chatview/src/widgets/chatview_state_widget.dart';
import 'package:chatview/src/widgets/reaction_popup.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

import '../extensions/extensions.dart';
import '../inherited_widgets/configurations_inherited_widgets.dart';
Expand Down Expand Up @@ -174,13 +175,42 @@ class ChatView extends StatefulWidget {
return state?._sendMessageKey.currentState?.replyMessage;
}

/// Static method to scroll to a specific index in the chat list
static Future<void> scrollToIndex(
BuildContext context,
int index, {
AutoScrollPosition? preferPosition,
}) async {
final state = context.findAncestorStateOfType<_ChatViewState>();
assert(
state != null,
'ChatViewState not found. Make sure to use correct context that contains the ChatViewState',
);
await state?.scrollToIndex(index, preferPosition: preferPosition);
}

/// Static method to scroll to a specific message by its ID
static Future<void> scrollToMessageById(
BuildContext context,
String messageId, {
AutoScrollPosition? preferPosition,
}) async {
final state = context.findAncestorStateOfType<_ChatViewState>();
assert(
state != null,
'ChatViewState not found. Make sure to use correct context that contains the ChatViewState',
);
await state?.scrollToMessageById(messageId, preferPosition: preferPosition);
}

@override
State<ChatView> createState() => _ChatViewState();
}

class _ChatViewState extends State<ChatView>
with SingleTickerProviderStateMixin {
final GlobalKey<SendMessageWidgetState> _sendMessageKey = GlobalKey();
final GlobalKey<ChatListWidgetState> _chatListKey = GlobalKey();

ChatController get chatController => widget.chatController;

Expand Down Expand Up @@ -287,6 +317,7 @@ class _ChatViewState extends State<ChatView>
?.unfocus(),
behavior: HitTestBehavior.opaque,
child: ChatListWidget(
key: _chatListKey,
chatController: widget.chatController,
loadMoreData: widget.loadMoreData,
isLastPage: widget.isLastPage,
Expand Down Expand Up @@ -366,6 +397,32 @@ class _ChatViewState extends State<ChatView>

void replyMessageViewClose() => _sendMessageKey.currentState?.onCloseTap();

/// Scroll to a specific index in the chat list
Future<void> scrollToIndex(int index,
{AutoScrollPosition? preferPosition}) async {
await _chatListKey.currentState?.scrollToIndex(
index,
preferPosition: preferPosition ?? AutoScrollPosition.begin,
);
}

/// Scroll to a specific message by its ID
Future<void> scrollToMessageById(
String messageId, {
AutoScrollPosition? preferPosition,
}) async {
final messages = chatController.initialMessageList;
final index = messages.indexWhere((message) => message.id == messageId);
if (index != -1) {
// Convert to list view index (list is reversed)
final listViewIndex = messages.length - 1 - index;
await scrollToIndex(
listViewIndex,
preferPosition: preferPosition ?? AutoScrollPosition.middle,
);
}
}

@override
void dispose() {
chatViewIW?.showPopUp.dispose();
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies:
intl: 0.20.2
url_launcher: 6.3.2

scroll_to_index: ^3.0.1
dev_dependencies:
flutter_lints: 6.0.0
flutter_test:
Expand Down
Loading