diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index 1c6293ad..afeb5f00 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -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), + ), + ], + ); + }, ); } } diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index 63a5712e..1190b050 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -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. diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 7c12646f..38a8e175 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -146,12 +146,12 @@ class _VideoChatState extends State { 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(