diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 037d4f04..d4b1e02a 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -241,6 +241,15 @@ abstract class ChatStoreBase with Store { @readonly var _isWaitingForAck = false; + /// Whether the chat WebSocket is currently connected. + @readonly + var _isConnected = false; + + /// Whether we've successfully connected at least once (to distinguish + /// "connecting" from "disconnected" in the UI). + @readonly + var _hasConnected = false; + /// The logged-in user's appearance in chat. @readonly var _userState = const USERSTATE(); @@ -251,12 +260,6 @@ abstract class ChatStoreBase with Store { @observable IRCMessage? replyingToMessage; - /// Public getter for whether the chat is in shared chat mode. - bool get isInSharedChatMode => _isInSharedChatMode; - - /// Public getter for whether we're waiting for message acknowledgment. - bool get isWaitingForAck => _isWaitingForAck; - /// Emotes matching the current autocomplete search term. @computed List get matchingEmotes { @@ -637,6 +640,10 @@ abstract class ChatStoreBase with Store { // Reset exponential backoff if successfully connected. _retries = 0; _backoffTime = 0; + + // Mark connection as established for UI + _isConnected = true; + _hasConnected = true; } } } @@ -839,6 +846,9 @@ abstract class ChatStoreBase with Store { // Invalidate the current connection to cancel pending delayed callbacks currentConnectionId = 0; + // Mark connection as disconnected for UI + _isConnected = false; + // Cancel chat delay countdown when disconnecting _cancelChatDelayCountdown(); _chatDelaySyncCompleted = false; diff --git a/lib/screens/channel/chat/stores/chat_store.g.dart b/lib/screens/channel/chat/stores/chat_store.g.dart index 8b3b585e..826915be 100644 --- a/lib/screens/channel/chat/stores/chat_store.g.dart +++ b/lib/screens/channel/chat/stores/chat_store.g.dart @@ -247,6 +247,46 @@ mixin _$ChatStore on ChatStoreBase, Store { }); } + late final _$_isConnectedAtom = Atom( + name: 'ChatStoreBase._isConnected', + context: context, + ); + + bool get isConnected { + _$_isConnectedAtom.reportRead(); + return super._isConnected; + } + + @override + bool get _isConnected => isConnected; + + @override + set _isConnected(bool value) { + _$_isConnectedAtom.reportWrite(value, super._isConnected, () { + super._isConnected = value; + }); + } + + late final _$_hasConnectedAtom = Atom( + name: 'ChatStoreBase._hasConnected', + context: context, + ); + + bool get hasConnected { + _$_hasConnectedAtom.reportRead(); + return super._hasConnected; + } + + @override + bool get _hasConnected => hasConnected; + + @override + set _hasConnected(bool value) { + _$_hasConnectedAtom.reportWrite(value, super._hasConnected, () { + super._hasConnected = value; + }); + } + late final _$_userStateAtom = Atom( name: 'ChatStoreBase._userState', context: context, diff --git a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart index aa6ee69a..b36883fa 100644 --- a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart +++ b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart @@ -245,7 +245,10 @@ class ChatBottomBar extends StatelessWidget { builder: (context) { final isLoggedIn = chatStore.auth.isLoggedIn; final isWaitingForAck = chatStore.isWaitingForAck; - final isEnabled = isLoggedIn && !isWaitingForAck; + final isConnected = chatStore.isConnected; + final hasConnected = chatStore.hasConnected; + final isEnabled = + isLoggedIn && !isWaitingForAck && isConnected; return GestureDetector( onTap: !isLoggedIn @@ -285,15 +288,19 @@ class ChatBottomBar extends StatelessWidget { ], ), hintMaxLines: 1, - hintText: isLoggedIn - ? isWaitingForAck - ? 'Sending...' - : chatStore.replyingToMessage != null - ? 'Reply' - : hasChatDelay - ? 'Chat (${chatStore.settings.chatDelay.toInt()}s delay)' - : 'Chat' - : loginTooltipMessage, + hintText: !isLoggedIn + ? loginTooltipMessage + : !isConnected + ? (hasConnected + ? 'Chat disconnected' + : 'Connecting...') + : isWaitingForAck + ? 'Sending...' + : chatStore.replyingToMessage != null + ? 'Reply' + : hasChatDelay + ? 'Chat (${chatStore.settings.chatDelay.toInt()}s delay)' + : 'Chat', ), controller: chatStore.textController, onSubmitted: chatStore.sendMessage, @@ -326,7 +333,8 @@ class ChatBottomBar extends StatelessWidget { : const Icon(Icons.send_rounded), onPressed: chatStore.auth.isLoggedIn && - !chatStore.isWaitingForAck + !chatStore.isWaitingForAck && + chatStore.isConnected ? () => chatStore.sendMessage( chatStore.textController.text, ) diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 009f9dfb..4d7e3649 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -215,7 +215,7 @@ abstract class _SettingsStoreBase with Store { @JsonKey(defaultValue: defaultShowUserNotices) @observable - var showUserNotices = defaultHighlightFirstTimeChatter; + var showUserNotices = defaultShowUserNotices; // Layout options @JsonKey(defaultValue: defaultEmoteMenuButtonOnLeft)