Fix chat scrolling behavior with keyboard
This commit is contained in:
parent
77e604cf50
commit
bcfe701b43
3 changed files with 123 additions and 139 deletions
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue