diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index c66dae36..6c130a8a 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -128,6 +128,12 @@ class _VideoChatState extends State if (state == AppLifecycleState.resumed) { _videoStore.handleAppResume(); + _chatTabsStore.onAppResumed(); + } else if (state == AppLifecycleState.paused) { + // No frames are produced while backgrounded, but Dart timers keep + // firing — stop the chat flush timers until resume. (Messages keep + // buffering; PiP hides chat anyway, so pausing during PiP is safe.) + _chatTabsStore.onAppPaused(); } } diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 356d09db..a905c5fb 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -189,6 +189,12 @@ abstract class ChatStoreBase with Store { /// The periodic timer used for batching chat message re-renders. Timer? _messageBufferTimer; + /// Whether the app is backgrounded. Gates the message flush timer — no + /// frames are produced while paused, so the 5x/s wakeups would be pure + /// battery cost. Messages keep parsing into [messageBuffer] and are flushed + /// on resume (with the overflow drain in [_handleIRCData] bounding growth). + var _isAppPaused = false; + /// The list of chat messages to add once autoscroll is resumed. /// This is used as an optimization to prevent the list from being updated/shifted while the user is scrolling. final messageBuffer = ObservableList(); @@ -817,8 +823,10 @@ abstract class ChatStoreBase with Store { continue; } - if (!_autoScroll) { - // While autoscroll is disabled, occasionally move messages from the buffer to the messages to prevent a memory leak. + if (!_autoScroll || _isAppPaused) { + // While the flush is held (scrolled up, or app backgrounded), + // occasionally move messages from the buffer to the messages to + // prevent unbounded buffer growth. if (messageBuffer.length >= _messagesToRemove) { _messages.addAll(messageBuffer); messageBuffer.clear(); @@ -841,13 +849,7 @@ abstract class ChatStoreBase with Store { ); // Activate the message buffer. - // Cancel any existing timer before creating a new one to prevent duplicates on reconnect. - _messageBufferTimer?.cancel(); - // Create a timer that will add messages from the buffer every 200 milliseconds. - _messageBufferTimer = Timer.periodic( - const Duration(milliseconds: 200), - (timer) => addMessages(), - ); + _startMessageBufferTimer(); // Set up 7TV real-time listener (assets already fetched in connectToChat) if (settings.show7TVEmotes) { @@ -1251,6 +1253,38 @@ abstract class ChatStoreBase with Store { } } + /// (Re)starts the periodic buffer flush. Cancels any existing timer first + /// to prevent duplicates on reconnect; no-ops while the app is backgrounded + /// ([onAppResumed] restarts it). + void _startMessageBufferTimer() { + _messageBufferTimer?.cancel(); + _messageBufferTimer = null; + if (_isAppPaused) return; + // Create a timer that will add messages from the buffer every 200 milliseconds. + _messageBufferTimer = Timer.periodic( + const Duration(milliseconds: 200), + (timer) => addMessages(), + ); + } + + /// Stops the flush timer while the app is backgrounded. + void onAppPaused() { + _isAppPaused = true; + _messageBufferTimer?.cancel(); + _messageBufferTimer = null; + } + + /// Restarts the flush timer after [onAppPaused] and flushes the backlog + /// immediately so chat is current on the first visible frame. + void onAppResumed() { + if (!_isAppPaused) return; + _isAppPaused = false; + if (_hasConnected) { + _startMessageBufferTimer(); + addMessages(); + } + } + @action void addMessages() { _flushDueDelayedCallbacks(); diff --git a/lib/screens/channel/chat/stores/chat_tabs_store.dart b/lib/screens/channel/chat/stores/chat_tabs_store.dart index ee095442..5b06cb78 100644 --- a/lib/screens/channel/chat/stores/chat_tabs_store.dart +++ b/lib/screens/channel/chat/stores/chat_tabs_store.dart @@ -407,6 +407,32 @@ abstract class ChatTabsStoreBase with Store { } } + /// Forwards app-pause to every activated tab and stops the merged render + /// timer — no frames are produced while the app is backgrounded, so the + /// periodic flush/recompute work would be pure battery cost. + void onAppPaused() { + _mergedRenderTimer?.cancel(); + _mergedRenderTimer = null; + for (final tab in _tabs) { + tab.chatStore?.onAppPaused(); + } + } + + /// Resumes tab flush timers and (if merged mode is active) the merged + /// render timer after [onAppPaused]. + void onAppResumed() { + for (final tab in _tabs) { + tab.chatStore?.onAppResumed(); + } + if (mergedMode && _mergedRenderTimer == null) { + _refreshMergedMessages(); + _mergedRenderTimer = Timer.periodic( + const Duration(milliseconds: 200), + (_) => _refreshMergedMessages(), + ); + } + } + /// Re-enables auto-scroll and jumps to the latest message in merged view. @action void resumeMergedScroll() { diff --git a/lib/screens/channel/chat/widgets/chat_tabs.dart b/lib/screens/channel/chat/widgets/chat_tabs.dart index 490ce0df..0321169b 100644 --- a/lib/screens/channel/chat/widgets/chat_tabs.dart +++ b/lib/screens/channel/chat/widgets/chat_tabs.dart @@ -112,17 +112,26 @@ class ChatTabs extends StatelessWidget { ) : IndexedStack( index: activeIndex, - children: tabs.map((tabInfo) { - if (tabInfo.chatStore == null) { - return const SizedBox.shrink(); - } - return Chat( - key: ValueKey(tabInfo.channelId), - chatStore: tabInfo.chatStore!, - listPadding: adjustedPadding, - onAddChat: () => _handleAddChat(context), - ); - }).toList(), + children: [ + for (var i = 0; i < tabs.length; i++) + if (tabs[i].chatStore == null) + const SizedBox.shrink() + else + // IndexedStack children stay mounted (and keep + // animating) when hidden. Disabling TickerMode + // pauses inactive tabs' animated emotes — the + // Image widget stops its frame-decode loop while + // the ticker mode is off and resumes on switch. + TickerMode( + key: ValueKey(tabs[i].channelId), + enabled: i == activeIndex, + child: Chat( + chatStore: tabs[i].chatStore!, + listPadding: adjustedPadding, + onAddChat: () => _handleAddChat(context), + ), + ), + ], ), ), if (showTabBar)