Fix chat scrolling behavior with keyboard

This commit is contained in:
Tommy Chow 2022-04-08 22:50:40 -04:00
parent 77e604cf50
commit bcfe701b43
3 changed files with 123 additions and 139 deletions

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart';
@ -14,103 +15,109 @@ class Chat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Observer(
builder: (context) => Column(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (chatStore.assetsStore.showEmoteMenu) {
chatStore.assetsStore.showEmoteMenu = false;
} else if (chatStore.textFieldFocusNode.hasFocus) {
chatStore.textFieldFocusNode.unfocus();
}
},
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale),
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
child: Observer(
builder: (context) {
final showDividers = chatStore.settings.showChatMessageDividers;
builder: (context) {
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (chatStore.scrollController.hasClients) chatStore.scrollController.jumpTo(chatStore.scrollController.position.maxScrollExtent);
});
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 5.0),
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: chatStore.messages.length,
controller: chatStore.scrollController,
separatorBuilder: (context, index) => showDividers
? Divider(
height: chatStore.settings.messageSpacing,
thickness: 1.0,
)
: SizedBox(height: chatStore.settings.messageSpacing),
itemBuilder: (context, index) => Observer(
builder: (context) {
final message = chatStore.messages[index];
final chatMessage = ChatMessage(
ircMessage: message,
assetsStore: chatStore.assetsStore,
settingsStore: chatStore.settings,
);
return Column(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (chatStore.assetsStore.showEmoteMenu) {
chatStore.assetsStore.showEmoteMenu = false;
} else if (chatStore.textFieldFocusNode.hasFocus) {
chatStore.textFieldFocusNode.unfocus();
}
},
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale),
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
child: Observer(
builder: (context) {
final showDividers = chatStore.settings.showChatMessageDividers;
if (message.user != null && message.user != chatStore.auth.user.details?.login) {
return InkWell(
onTap: () {
FocusScope.of(context).unfocus();
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
},
onLongPress: () => showModalBottomSheet(
context: context,
builder: (context) => ChatUserModal(
chatStore: chatStore,
username: message.user!,
userId: message.tags['user-id']!,
displayName: message.tags['display-name']!,
),
),
child: chatMessage,
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 5.0),
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: chatStore.messages.length,
controller: chatStore.scrollController,
separatorBuilder: (context, index) => showDividers
? Divider(
height: chatStore.settings.messageSpacing,
thickness: 1.0,
)
: SizedBox(height: chatStore.settings.messageSpacing),
itemBuilder: (context, index) => Observer(
builder: (context) {
final message = chatStore.messages[index];
final chatMessage = ChatMessage(
ircMessage: message,
assetsStore: chatStore.assetsStore,
settingsStore: chatStore.settings,
);
}
return chatMessage;
},
),
);
},
if (message.user != null && message.user != chatStore.auth.user.details?.login) {
return InkWell(
onTap: () {
FocusScope.of(context).unfocus();
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
},
onLongPress: () => showModalBottomSheet(
context: context,
builder: (context) => ChatUserModal(
chatStore: chatStore,
username: message.user!,
userId: message.tags['user-id']!,
displayName: message.tags['display-name']!,
),
),
child: chatMessage,
);
}
return chatMessage;
},
),
);
},
),
),
),
),
Observer(
builder: (_) => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: chatStore.autoScroll
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
width: double.infinity,
child: ElevatedButton.icon(
onPressed: chatStore.resumeScroll,
label: const Text('Resume Scroll'),
icon: const Icon(Icons.arrow_circle_down),
Observer(
builder: (_) => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: chatStore.autoScroll
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
width: double.infinity,
child: ElevatedButton.icon(
onPressed: chatStore.resumeScroll,
label: const Text('Resume Scroll'),
icon: const Icon(Icons.arrow_circle_down),
),
),
),
),
),
),
],
],
),
),
),
),
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
if (chatStore.assetsStore.showEmoteMenu)
SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: EmoteMenu(chatStore: chatStore),
),
],
),
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
if (chatStore.assetsStore.showEmoteMenu)
SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: EmoteMenu(chatStore: chatStore),
),
],
);
},
);
}
}

View file

@ -90,24 +90,6 @@ abstract class _ChatStoreBase with Store {
required this.channelId,
required this.displayName,
}) {
// Create a reaction where anytime the emote menu is shown or hidden,
// scroll to the bottom of the list. This will prevent the emote menu
// from covering the latest messages when summoned.
reactions.add(reaction(
(_) => assetsStore.showEmoteMenu,
(_) => SchedulerBinding.instance?.addPostFrameCallback((_) {
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
}),
));
// Create a reaction for scrolling to bottom when exiting fullscreen.
reactions.add(reaction(
(_) => settings.fullScreen,
(_) => SchedulerBinding.instance?.addPostFrameCallback((_) {
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
}),
));
// Create a reaction that will reconnect to chat when logging in or out.
// Closing the channel will trigger a reconnect with the new credentials.
reactions.add(reaction(
@ -132,18 +114,9 @@ abstract class _ChatStoreBase with Store {
}
});
// Add a listener to the textfield focus that will do the following when focused:
// 1. Hide the emote menu if it is currently shown
// 2. Scroll to the latest message (after a brief delay)
// Add a listener to the textfield focus that will hide the emote menu if it is currently shown.
textFieldFocusNode.addListener(() {
if (textFieldFocusNode.hasFocus) {
if (assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
Future.delayed(
const Duration(milliseconds: 500),
() => scrollController.jumpTo(scrollController.position.maxScrollExtent),
);
}
if (textFieldFocusNode.hasFocus && assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
});
// Add a listener to the textfield that will show/hide the autocomplete bar if focused.
@ -327,29 +300,33 @@ abstract class _ChatStoreBase with Store {
if (_channel == null || _channel?.closeCode != null) {
_messages.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
return;
}
} else {
// Send the message to the IRC chat room.
_channel?.sink.add('PRIVMSG #$channelName :$message');
// Send the message to the IRC chat room.
_channel?.sink.add('PRIVMSG #$channelName :$message');
// Obtain the logged-in user's appearance in chat with USERSTATE and create the full message to render.
var userStateString = _userState.raw;
if (userStateString != null) {
if (message.length > 3 && message.substring(0, 3) == '/me') {
userStateString += ' :\x01ACTION ${message.replaceRange(0, 3, '').trim()}\x01';
} else {
userStateString += ' :' + message.trim();
}
// Obtain the logged-in user's appearance in chat with USERSTATE and create the full message to render.
var userStateString = _userState.raw;
if (userStateString != null) {
if (message.length > 3 && message.substring(0, 3) == '/me') {
userStateString += ' :\x01ACTION ${message.replaceRange(0, 3, '').trim()}\x01';
} else {
userStateString += ' :' + message.trim();
final userChatMessage = IRCMessage.fromString(userStateString);
userChatMessage.localEmotes?.addAll(assetsStore.userEmoteToObject);
toSend = userChatMessage;
}
final userChatMessage = IRCMessage.fromString(userStateString);
userChatMessage.localEmotes?.addAll(assetsStore.userEmoteToObject);
toSend = userChatMessage;
// Clear the previous input in the TextField.
textController.clear();
}
// Clear the previous input in the TextField.
textController.clear();
// Scroll to the latest message after sending.
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
/// Adds the given [emote] to the chat textfield.

View file

@ -146,12 +146,12 @@ class _VideoChatState extends State<VideoChat> {
return Scaffold(
body: OrientationBuilder(
builder: (context, orientation) {
if (orientation == Orientation.landscape) {
// Add a post-frame callback to scroll to bottom when rotating.
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (_chatStore.scrollController.hasClients) _chatStore.scrollController.jumpTo(_chatStore.scrollController.position.maxScrollExtent);
});
// Scroll to bottom when summoning keyboard or rotating.
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (_chatStore.scrollController.hasClients) _chatStore.scrollController.jumpTo(_chatStore.scrollController.position.maxScrollExtent);
});
if (orientation == Orientation.landscape) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
return Observer(