diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index 7a30638a..a2f88df2 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -73,18 +73,19 @@ class Chat extends StatelessWidget { color: Colors.white, ), ), - Button( - onPressed: () async { - // Paste clipboard text into the text controller. - final data = await Clipboard.getData(Clipboard.kTextPlain); + if (chatStore.notification!.contains('copied')) + Button( + onPressed: () async { + // Paste clipboard text into the text controller. + final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) chatStore.textController.text = data.text!; + if (data != null) chatStore.textController.text = data.text!; - chatStore.updateNotification(''); - }, - fill: false, - child: const Text('Paste'), - ), + chatStore.updateNotification(''); + }, + fill: false, + child: const Text('Paste'), + ), ], ), ), diff --git a/lib/screens/channel/chat/emote_menu/emote_menu.dart b/lib/screens/channel/chat/emote_menu/emote_menu.dart index de8f8011..d277dc84 100644 --- a/lib/screens/channel/chat/emote_menu/emote_menu.dart +++ b/lib/screens/channel/chat/emote_menu/emote_menu.dart @@ -4,7 +4,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart'; import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; -import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/button.dart'; class EmoteMenu extends StatefulWidget { @@ -68,28 +67,6 @@ class _EmoteMenuState extends State { ), ), ), - Button( - onPressed: () async { - await widget.chatStore.getAssets(); - - if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: AlertMessage(message: 'Emotes refreshed'), - behavior: SnackBarBehavior.floating, - ), - ); - }, - color: Theme.of(context).colorScheme.secondary, - child: const Text( - 'REFRESH EMOTES', - style: TextStyle( - letterSpacing: 0.8, - fontWeight: FontWeight.w600, - ), - ), - ), ], ), ), diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index bd7b22ca..3bc61dc7 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -49,6 +49,9 @@ abstract class ChatStoreBase with Store { /// The Twitch IRC WebSocket channel. WebSocketChannel? _channel; + /// The subscription that handles the WebSocket connection. + StreamSubscription? _channelListener; + // The retry counter for exponential backoff. var _retries = 0; @@ -322,7 +325,7 @@ abstract class ChatStoreBase with Store { _channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443')); // Listen for new messages and forward them to the handler. - _channel?.stream.listen( + _channelListener = _channel?.stream.listen( (data) => Future.delayed(Duration(seconds: settings.chatDelay.toInt()), () => _handleIRCData(data.toString())), onError: (error) => debugPrint('Chat error: ${error.toString()}'), onDone: () async { @@ -342,6 +345,7 @@ abstract class ChatStoreBase with Store { // Increment the retry count and attempt the reconnect. _retries++; _messageBuffer.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...')); + _channelListener?.cancel(); connectToChat(); }, ); @@ -453,6 +457,7 @@ abstract class ChatStoreBase with Store { _channel?.sink.close(1001); _channel = null; + _channelListener?.cancel(); for (final reactionDisposer in reactions) { reactionDisposer(); diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 134e9bc5..e750aba5 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -77,6 +77,67 @@ class _VideoChatState extends State { Widget build(BuildContext context) { final settingsStore = _chatStore.settings; + void showSettings() { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (context) => FrostyModal( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.app_settings_alt), + title: const Text('App settings'), + onTap: () => showModalBottomSheet( + backgroundColor: Colors.transparent, + isScrollControlled: true, + context: context, + builder: (context) => FrostyModal( + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: Settings(settingsStore: settingsStore), + ), + ), + ), + ), + ListTile( + leading: const Icon(Icons.refresh), + title: const Text('Reconnect to chat'), + onTap: () { + _chatStore.updateNotification('Reconnecting to chat...'); + + _chatStore.connectToChat(); + }, + ), + ListTile( + leading: const Icon(Icons.refresh), + title: const Text('Refresh badges and emotes'), + onTap: () async { + await _chatStore.getAssets(); + + _chatStore.updateNotification('Badges and emotes refreshed'); + }, + ), + ], + ), + ), + ); + } + + final appBar = AppBar( + title: Text( + regexEnglish.hasMatch(_chatStore.displayName) ? _chatStore.displayName : '${_chatStore.displayName} (${_chatStore.channelName})', + style: const TextStyle(fontSize: 20), + ), + actions: [ + IconButton( + tooltip: 'Settings', + icon: const Icon(Icons.settings), + onPressed: showSettings, + ), + ], + ); + final player = GestureDetector( onLongPress: _videoStore.handleToggleOverlay, child: _Video( @@ -85,7 +146,10 @@ class _VideoChatState extends State { ), ); - final videoOverlay = _VideoOverlay(videoStore: _videoStore); + final videoOverlay = _VideoOverlay( + videoStore: _videoStore, + onSettingsPressed: showSettings, + ); final overlay = GestureDetector( onLongPress: _videoStore.handleToggleOverlay, @@ -138,30 +202,6 @@ class _VideoChatState extends State { chatStore: _chatStore, ); - final appBar = AppBar( - title: Text( - regexEnglish.hasMatch(_chatStore.displayName) ? _chatStore.displayName : '${_chatStore.displayName} (${_chatStore.channelName})', - style: const TextStyle(fontSize: 20), - ), - actions: [ - IconButton( - tooltip: 'Settings', - icon: const Icon(Icons.settings), - onPressed: () => showModalBottomSheet( - backgroundColor: Colors.transparent, - isScrollControlled: true, - context: context, - builder: (context) => FrostyModal( - child: SizedBox( - height: MediaQuery.of(context).size.height * 0.8, - child: Settings(settingsStore: settingsStore), - ), - ), - ), - ), - ], - ); - final videoChat = Scaffold( body: OrientationBuilder( builder: (context, orientation) { @@ -306,8 +346,13 @@ class _Video extends StatelessWidget { /// Creates a widget containing controls which enable interactions with an underlying [_Video] widget. class _VideoOverlay extends StatelessWidget { final VideoStore videoStore; + final void Function() onSettingsPressed; - const _VideoOverlay({Key? key, required this.videoStore}) : super(key: key); + const _VideoOverlay({ + Key? key, + required this.videoStore, + required this.onSettingsPressed, + }) : super(key: key); Future _showSleepTimerDialog(BuildContext context) { return showDialog( @@ -401,17 +446,7 @@ class _VideoOverlay extends StatelessWidget { Icons.settings, color: Colors.white, ), - onPressed: () => showModalBottomSheet( - backgroundColor: Colors.transparent, - isScrollControlled: true, - context: context, - builder: (context) => FrostyModal( - child: SizedBox( - height: MediaQuery.of(context).size.height * 0.8, - child: Settings(settingsStore: videoStore.settingsStore), - ), - ), - ), + onPressed: onSettingsPressed, ); final chatOverlayButton = Observer(