From 870e8ae9164f34b4115572ccd58fbef3e9ce9bcf Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Thu, 28 Jul 2022 21:44:28 -0400 Subject: [PATCH] Implement custom copy notification When copying a message in chat, a notification will now show under the video rather than the block the textfield. --- lib/screens/channel/chat/chat.dart | 148 ++++++++++-------- .../channel/chat/widgets/chat_message.dart | 8 +- lib/screens/channel/stores/chat_store.dart | 27 +++- lib/screens/channel/stores/chat_store.g.dart | 17 ++ lib/widgets/alert_message.dart | 8 +- lib/widgets/button.dart | 2 +- 6 files changed, 134 insertions(+), 76 deletions(-) diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index 91d9542e..ae2ae82d 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -4,6 +4,7 @@ import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_message.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; +import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/button.dart'; class Chat extends StatelessWidget { @@ -15,74 +16,99 @@ class Chat extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (context) { - return Column( + return Stack( 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) { - return ListView.builder( - reverse: true, - padding: EdgeInsets.zero, - addAutomaticKeepAlives: false, - controller: chatStore.scrollController, - itemCount: chatStore.renderMessages.length, - itemBuilder: (context, index) => ChatMessage( - ircMessage: chatStore.renderMessages.reversed.toList()[index], - chatStore: chatStore, - ), - ); - }, + 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) { + return ListView.builder( + reverse: true, + padding: EdgeInsets.zero, + addAutomaticKeepAlives: false, + controller: chatStore.scrollController, + itemCount: chatStore.renderMessages.length, + itemBuilder: (context, index) => ChatMessage( + ircMessage: chatStore.renderMessages.reversed.toList()[index], + chatStore: chatStore, + ), + ); + }, + ), + ), ), - ), - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Observer( - builder: (_) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: chatStore.autoScroll - ? null - : Button( - padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), - onPressed: chatStore.resumeScroll, - icon: const Icon(Icons.arrow_circle_down), - child: const Text('Resume Scroll'), - ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Observer( + builder: (_) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + child: chatStore.autoScroll + ? null + : Button( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + onPressed: chatStore.resumeScroll, + icon: const Icon(Icons.arrow_circle_down), + child: const Text('Resume Scroll'), + ), + ), + ), ), - ), + ], ), - ], + ), ), - ), + if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore), + AnimatedContainer( + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0, + child: AnimatedOpacity( + curve: Curves.ease, + opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: EmoteMenu(chatStore: chatStore), + ), + ), + ], ), - if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore), - AnimatedContainer( - curve: Curves.ease, + AnimatedSwitcher( duration: const Duration(milliseconds: 200), - height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0, - child: AnimatedOpacity( - curve: Curves.ease, - opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0, - duration: const Duration(milliseconds: 200), - child: EmoteMenu(chatStore: chatStore), - ), + child: chatStore.notification != null + ? Align( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.8), + borderRadius: BorderRadius.circular(30.0), + ), + width: double.infinity, + padding: const EdgeInsets.all(10.0), + margin: const EdgeInsets.all(10.0), + child: AlertMessage( + message: chatStore.notification!, + color: Colors.white, + ), + ), + ) + : null, ), ], ); diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index a52e71dc..d15540b4 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -4,7 +4,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/models/irc.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; -import 'package:frosty/widgets/alert_message.dart'; class ChatMessage extends StatelessWidget { final IRCMessage ircMessage; @@ -45,12 +44,7 @@ class ChatMessage extends StatelessWidget { Clipboard.setData(ClipboardData(text: ircMessage.message)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: AlertMessage(message: 'Message copied to clipboard'), - behavior: SnackBarBehavior.floating, - ), - ); + chatStore.notification = 'Message copied'; } return Observer( diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index 5a55e20e..a4cb8051 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -112,6 +112,13 @@ abstract class ChatStoreBase with Store { @observable var expandChat = false; + /// A notification message to display above the chat. + @observable + String? notification; + + /// Timer used for dismissing the notification. + Timer? _notificationTimer; + ChatStoreBase({ required this.auth, required this.chatDetailsStore, @@ -126,10 +133,22 @@ abstract class ChatStoreBase with Store { // 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( - (_) => auth.isLoggedIn, - (_) => _channel?.sink.close(1001), - )); + reactions.add( + reaction( + (_) => auth.isLoggedIn, + (_) => _channel?.sink.close(1001), + ), + ); + + reactions.add( + reaction( + (_) => notification, + (_) { + if (_notificationTimer != null) _notificationTimer?.cancel(); + _notificationTimer = Timer(const Duration(seconds: 2), () => notification = null); + }, + ), + ); // Create a timer that will add messages from the buffer every 200 milliseconds. _messageBufferTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) => addMessages()); diff --git a/lib/screens/channel/stores/chat_store.g.dart b/lib/screens/channel/stores/chat_store.g.dart index 81e4d79b..f5d16aad 100644 --- a/lib/screens/channel/stores/chat_store.g.dart +++ b/lib/screens/channel/stores/chat_store.g.dart @@ -125,6 +125,22 @@ mixin _$ChatStore on ChatStoreBase, Store { }); } + late final _$notificationAtom = + Atom(name: 'ChatStoreBase.notification', context: context); + + @override + String? get notification { + _$notificationAtom.reportRead(); + return super.notification; + } + + @override + set notification(String? value) { + _$notificationAtom.reportWrite(value, super.notification, () { + super.notification = value; + }); + } + late final _$getAssetsAsyncAction = AsyncAction('ChatStoreBase.getAssets', context: context); @@ -206,6 +222,7 @@ mixin _$ChatStore on ChatStoreBase, Store { String toString() { return ''' expandChat: ${expandChat}, +notification: ${notification}, renderMessages: ${renderMessages} '''; } diff --git a/lib/widgets/alert_message.dart b/lib/widgets/alert_message.dart index 34b661f0..613ed9c2 100644 --- a/lib/widgets/alert_message.dart +++ b/lib/widgets/alert_message.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; class AlertMessage extends StatelessWidget { final String message; final IconData icon; + final Color? color; const AlertMessage({ Key? key, required this.message, final this.icon = Icons.info, + this.color, }) : super(key: key); @override @@ -18,15 +20,15 @@ class AlertMessage extends StatelessWidget { children: [ Icon( icon, - color: Colors.grey, + color: color ?? Colors.grey, ), const SizedBox(width: 5.0), Flexible( child: Text( message, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w500, - color: Colors.grey, + color: color ?? Colors.grey, ), ), ), diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 1dddf96c..31cd17cf 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -40,7 +40,7 @@ class _ButtonState extends State