Skip to content

Commit 99b6aa4

Browse files
authored
Improve chat auto-scroll behavior with keyboard visibility in chat screens (#658)
* refactor: improve chat auto-scroll behavior with keyboard visibility tracking * chore:precommit changes * chore: updated changelog
1 parent 12f5238 commit 99b6aa4

File tree

2 files changed

+64
-20
lines changed

2 files changed

+64
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Fixes wrong relay status error when switching accounts
3333
- Fixes scroll to bototm inside of chats
3434
- Fixed profile image not showing up chatlist after login
35+
- Fixes auto-scroll to bottom when keyboard opens
3536

3637
### Security
3738

lib/ui/chat/chat_screen.dart

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,38 +39,45 @@ class ChatScreen extends ConsumerStatefulWidget {
3939
ConsumerState<ChatScreen> createState() => _ChatScreenState();
4040
}
4141

42-
class _ChatScreenState extends ConsumerState<ChatScreen> {
42+
class _ChatScreenState extends ConsumerState<ChatScreen> with WidgetsBindingObserver {
4343
final ScrollController _scrollController = ScrollController();
4444
double _lastScrollOffset = 0.0;
4545
Future<DMChatData?>? _dmChatDataFuture;
4646
ProviderSubscription<ChatState>? _chatSubscription;
47+
bool _hasInitialScrollCompleted = false;
48+
bool _isKeyboardOpen = false;
4749

4850
@override
4951
void initState() {
5052
super.initState();
53+
WidgetsBinding.instance.addObserver(this);
5154
_initializeDMChatData();
55+
5256
WidgetsBinding.instance.addPostFrameCallback((_) {
5357
if (widget.inviteId == null) {
5458
ref.read(groupsProvider.notifier).loadGroupDetails(widget.groupId);
5559
ref.read(chatProvider.notifier).loadMessagesForGroup(widget.groupId);
5660
}
5761
});
5862

63+
// Listen for chat state changes to handle auto-scroll
5964
_chatSubscription = ref.listenManual(chatProvider, (previous, next) {
60-
_handleScrollOnChatStateChange(previous, next);
65+
_handleChatStateChange(previous, next);
6166
});
6267
}
6368

6469
@override
6570
void didUpdateWidget(ChatScreen oldWidget) {
6671
super.didUpdateWidget(oldWidget);
6772
if (oldWidget.groupId != widget.groupId) {
73+
_hasInitialScrollCompleted = false; // Reset for new chat
6874
_initializeDMChatData();
6975
}
7076
}
7177

7278
@override
7379
void dispose() {
80+
WidgetsBinding.instance.removeObserver(this);
7481
_chatSubscription?.close();
7582
_scrollController.dispose();
7683
super.dispose();
@@ -84,37 +91,73 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
8491
}
8592
}
8693

87-
void _handleScrollToBottom({bool hasAnimation = true}) {
94+
/// Scroll to the bottom of the chat
95+
void _scrollToBottom({bool animated = true}) {
8896
WidgetsBinding.instance.addPostFrameCallback((_) {
8997
if (!_scrollController.hasClients || !mounted) return;
90-
final double max = _scrollController.position.maxScrollExtent;
91-
if (hasAnimation) {
98+
99+
final maxScrollExtent = _scrollController.position.maxScrollExtent;
100+
101+
if (animated) {
92102
_scrollController.animateTo(
93-
max,
94-
duration: const Duration(milliseconds: 200),
95-
curve: Curves.easeOut,
103+
maxScrollExtent,
104+
duration: const Duration(milliseconds: 300),
105+
curve: Curves.easeOutCubic,
96106
);
97107
} else {
98-
_scrollController.jumpTo(max);
108+
_scrollController.jumpTo(maxScrollExtent);
99109
}
100110
});
101111
}
102112

103-
void _handleScrollOnChatStateChange(
104-
ChatState? previous,
105-
ChatState next,
106-
) {
113+
/// Handle keyboard visibility changes
114+
@override
115+
void didChangeMetrics() {
116+
super.didChangeMetrics();
117+
118+
final bottomInset = WidgetsBinding.instance.platformDispatcher.views.first.viewInsets.bottom;
119+
final keyboardHeight =
120+
bottomInset / WidgetsBinding.instance.platformDispatcher.views.first.devicePixelRatio;
121+
122+
// Simple keyboard state tracking
123+
if (keyboardHeight > 100) {
124+
// Keyboard is open
125+
if (!_isKeyboardOpen) {
126+
_isKeyboardOpen = true;
127+
// Scroll to bottom when keyboard opens (with delay)
128+
Future.delayed(const Duration(milliseconds: 300), () {
129+
if (mounted && _isKeyboardOpen) {
130+
_scrollToBottom();
131+
}
132+
});
133+
}
134+
} else {
135+
// Keyboard is closed
136+
_isKeyboardOpen = false;
137+
}
138+
}
139+
140+
/// Handle chat state changes for auto-scroll
141+
void _handleChatStateChange(ChatState? previous, ChatState next) {
107142
final currentMessages = next.groupMessages[widget.groupId] ?? [];
108143
final previousMessages = previous?.groupMessages[widget.groupId] ?? [];
109144
final wasLoading = previous?.isGroupLoading(widget.groupId) ?? false;
110145
final isLoading = next.isGroupLoading(widget.groupId);
111146
final isLoadingCompleted = wasLoading && !isLoading;
112-
if (isLoadingCompleted && currentMessages.isNotEmpty) {
113-
_handleScrollToBottom(hasAnimation: false);
114-
} else if (previousMessages.isNotEmpty &&
147+
148+
// Auto-scroll when chat first loads
149+
if (isLoadingCompleted && currentMessages.isNotEmpty && !_hasInitialScrollCompleted) {
150+
_hasInitialScrollCompleted = true;
151+
_scrollToBottom(animated: false);
152+
return;
153+
}
154+
155+
// Auto-scroll when new messages arrive (after initial load)
156+
if (_hasInitialScrollCompleted &&
157+
previousMessages.isNotEmpty &&
115158
currentMessages.length > previousMessages.length &&
116159
currentMessages.last.id != previousMessages.last.id) {
117-
_handleScrollToBottom();
160+
_scrollToBottom();
118161
}
119162
}
120163

@@ -240,7 +283,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
240283
onNotification: (scrollInfo) {
241284
if (scrollInfo is ScrollUpdateNotification) {
242285
final currentFocus = FocusManager.instance.primaryFocus;
243-
if (currentFocus != null && currentFocus.hasFocus) {
286+
if (currentFocus != null && currentFocus.hasFocus && !_isKeyboardOpen) {
244287
final currentOffset = scrollInfo.metrics.pixels;
245288
final scrollDelta = currentOffset - _lastScrollOffset;
246289
if (scrollDelta < -20) currentFocus.unfocus();
@@ -402,16 +445,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
402445
groupId: widget.groupId,
403446
replyToMessageId: replyingTo.id,
404447
message: message,
405-
onMessageSent: _handleScrollToBottom,
406448
);
407449
} else {
408450
await chatNotifier.sendMessage(
409451
groupId: widget.groupId,
410452
message: message,
411453
isEditing: isEditing,
412-
onMessageSent: _handleScrollToBottom,
413454
);
414455
}
456+
// Auto-scroll after sending message
457+
_scrollToBottom();
415458
},
416459
),
417460
],

0 commit comments

Comments
 (0)