diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index 2b738741..ffaa0650 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.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'; import 'package:frosty/screens/channel/chat/widgets/chat_message.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/button.dart'; @@ -37,55 +35,15 @@ class Chat extends StatelessWidget { style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize), child: Observer( builder: (context) { - final showDividers = chatStore.settings.showChatMessageDividers; - - return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 5.0), + return ListView.builder( + padding: EdgeInsets.zero, 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, - ); - - 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: () { - HapticFeedback.lightImpact(); - - showModalBottomSheet( - backgroundColor: Colors.transparent, - isScrollControlled: true, - context: context, - builder: (context) => ChatUserModal( - chatStore: chatStore, - username: message.user!, - userId: message.tags['user-id']!, - displayName: message.tags['display-name']!, - ), - ); - }, - child: chatMessage, - ); - } - return chatMessage; - }, + itemBuilder: (context, index) => ChatMessage( + ircMessage: chatStore.messages[index], + chatStore: chatStore, ), ); }, diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index cc463fb5..8db4612e 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -1,160 +1,209 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/models/irc.dart'; -import 'package:frosty/screens/channel/stores/chat_assets_store.dart'; -import 'package:frosty/screens/settings/stores/settings_store.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; - final ChatAssetsStore assetsStore; - final SettingsStore settingsStore; + final ChatStore chatStore; + final bool isModal; const ChatMessage({ Key? key, required this.ircMessage, - required this.assetsStore, - required this.settingsStore, + required this.chatStore, + this.isModal = false, }) : super(key: key); @override Widget build(BuildContext context) { - const highlightPadding = EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0); - const messagePadding = EdgeInsets.symmetric(horizontal: 10.0); + return Observer( + builder: (context) { + Color? color; + final Widget renderMessage; + switch (ircMessage.command) { + case Command.privateMessage: + case Command.userState: + // If user is being mentioned in the message, highlight it red. + if (ircMessage.mention == true) color = Colors.red.withOpacity(0.3); - switch (ircMessage.command) { - case Command.privateMessage: - case Command.userState: - // Render normal chat message (PRIVMSG). - final child = Observer( - builder: (context) => Text.rich( - TextSpan( - children: ircMessage.generateSpan( - style: DefaultTextStyle.of(context).style, - assetsStore: assetsStore, - emoteScale: settingsStore.emoteScale, - badgeScale: settingsStore.badgeScale, - useZeroWidth: settingsStore.showZeroWidth, - useReadableColors: settingsStore.useReadableColors, - isLightTheme: Theme.of(context).brightness == Brightness.light, - launchExternal: settingsStore.launchUrlExternal, - timestamp: settingsStore.timestampType, + renderMessage = Text.rich( + TextSpan( + children: ircMessage.generateSpan( + style: DefaultTextStyle.of(context).style, + assetsStore: chatStore.assetsStore, + emoteScale: chatStore.settings.emoteScale, + badgeScale: chatStore.settings.badgeScale, + useZeroWidth: chatStore.settings.showZeroWidth, + useReadableColors: chatStore.settings.useReadableColors, + isLightTheme: Theme.of(context).brightness == Brightness.light, + launchExternal: chatStore.settings.launchUrlExternal, + timestamp: chatStore.settings.timestampType, + ), ), - ), - ), - ); + ); + break; + case Command.clearChat: + case Command.clearMessage: + // Render timeouts and bans + final banDuration = ircMessage.tags['ban-duration']; + renderMessage = Opacity( + opacity: 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: ircMessage.generateSpan( + style: DefaultTextStyle.of(context).style, + assetsStore: chatStore.assetsStore, + emoteScale: chatStore.settings.emoteScale, + badgeScale: chatStore.settings.badgeScale, + showMessage: chatStore.settings.showDeletedMessages, + useZeroWidth: chatStore.settings.showZeroWidth, + useReadableColors: chatStore.settings.useReadableColors, + isLightTheme: Theme.of(context).brightness == Brightness.light, + launchExternal: chatStore.settings.launchUrlExternal, + timestamp: chatStore.settings.timestampType, + ), + ), + ), + const SizedBox(height: 5.0), + if (banDuration == null) + if (ircMessage.command == Command.clearMessage) + const Text( + 'Message deleted', + style: TextStyle(fontWeight: FontWeight.w600), + ) + else + const Text( + 'User permanently banned', + style: TextStyle(fontWeight: FontWeight.w600), + ) + else + Text( + 'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}', + style: const TextStyle(fontWeight: FontWeight.w600), + ) + ], + ), + ); + break; + case Command.notice: + renderMessage = Text.rich( + TextSpan(text: ircMessage.message), + style: TextStyle(color: Theme.of(context).textTheme.bodyText2?.color?.withOpacity(0.5)), + ); + break; + case Command.userNotice: + color = Colors.deepPurple.withOpacity(0.3); - if (ircMessage.mention == true) { - return Container( - padding: highlightPadding, - color: const Color(0x4DFF0000), - child: child, - ); - } - return Padding( - padding: messagePadding, - child: child, - ); - case Command.clearChat: - case Command.clearMessage: - // Render timeouts and bans - final banDuration = ircMessage.tags['ban-duration']; - return Padding( - padding: highlightPadding, - child: Opacity( - opacity: 0.50, - child: Column( + renderMessage = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Observer( - builder: (context) => Text.rich( - TextSpan( - children: ircMessage.generateSpan( - style: DefaultTextStyle.of(context).style, - assetsStore: assetsStore, - emoteScale: settingsStore.emoteScale, - badgeScale: settingsStore.badgeScale, - showMessage: settingsStore.showDeletedMessages, - useZeroWidth: settingsStore.showZeroWidth, - useReadableColors: settingsStore.useReadableColors, - isLightTheme: Theme.of(context).brightness == Brightness.light, - launchExternal: settingsStore.launchUrlExternal, - timestamp: settingsStore.timestampType, - ), - ), - ), - ), - const SizedBox(height: 5.0), - if (banDuration == null) - if (ircMessage.command == Command.clearMessage) - const Text( - 'Message deleted', - style: TextStyle(fontWeight: FontWeight.bold), - ) - else - const Text( - 'User permanently banned', - style: TextStyle(fontWeight: FontWeight.bold), - ) - else + if (ircMessage.tags.containsKey('system-msg')) Text( - 'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}', - style: const TextStyle(fontWeight: FontWeight.bold), - ) - ], - ), - ), - ); - case Command.notice: - return Padding( - padding: messagePadding, - child: Text.rich( - TextSpan(text: ircMessage.message), - style: TextStyle(color: Theme.of(context).textTheme.bodyText2?.color?.withOpacity(0.5)), - ), - ); - case Command.userNotice: - return Container( - padding: highlightPadding, - color: const Color(0x339146FF), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (ircMessage.tags.containsKey('system-msg')) Text(ircMessage.tags['system-msg']!), - if (ircMessage.tags.containsKey('msg-id') && ircMessage.tags['msg-id'] == 'announcement') - Row( - children: const [ - Icon(Icons.announcement), - SizedBox(width: 5.0), - Text( - 'Announcement', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 5.0), - if (ircMessage.message != null) - Observer( - builder: (context) => Text.rich( + ircMessage.tags['system-msg']!, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + if (ircMessage.tags.containsKey('msg-id') && ircMessage.tags['msg-id'] == 'announcement') + Row( + children: const [ + Icon(Icons.announcement), + SizedBox(width: 5.0), + Text( + 'Announcement', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + const SizedBox(height: 5.0), + if (ircMessage.message != null) + Text.rich( TextSpan( children: ircMessage.generateSpan( style: DefaultTextStyle.of(context).style, - assetsStore: assetsStore, - emoteScale: settingsStore.emoteScale, - badgeScale: settingsStore.badgeScale, - useZeroWidth: settingsStore.showZeroWidth, - useReadableColors: settingsStore.useReadableColors, + assetsStore: chatStore.assetsStore, + emoteScale: chatStore.settings.emoteScale, + badgeScale: chatStore.settings.badgeScale, + useZeroWidth: chatStore.settings.showZeroWidth, + useReadableColors: chatStore.settings.useReadableColors, isLightTheme: Theme.of(context).brightness == Brightness.light, - launchExternal: settingsStore.launchUrlExternal, - timestamp: settingsStore.timestampType, + launchExternal: chatStore.settings.launchUrlExternal, + timestamp: chatStore.settings.timestampType, ), ), ), - ), - ], - ), + ], + ); + break; + default: + renderMessage = const SizedBox(); + } + + final paddedMessage = Padding( + padding: EdgeInsets.symmetric(vertical: chatStore.settings.messageSpacing / 2, horizontal: 10.0), + child: renderMessage, ); - default: - return const SizedBox(); - } + + // Add a divider above the message if dividers are enabled. + final dividedMessage = chatStore.settings.showChatMessageDividers + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + paddedMessage, + const Divider( + height: 1.0, + thickness: 1.0, + ), + ], + ) + : paddedMessage; + + // Color the message if the color has been set. + final coloredMessage = color == null ? dividedMessage : ColoredBox(color: color, child: dividedMessage); + + return InkWell( + onTap: () { + FocusScope.of(context).unfocus(); + if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false; + }, + onLongPress: isModal + ? () { + HapticFeedback.lightImpact(); + + Clipboard.setData(ClipboardData(text: ircMessage.message)); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: AlertMessage(message: 'Message copied to clipboard'), + behavior: SnackBarBehavior.floating, + ), + ); + } + : ircMessage.user != null && ircMessage.user != chatStore.auth.user.details?.login + ? () { + HapticFeedback.lightImpact(); + + showModalBottomSheet( + backgroundColor: Colors.transparent, + isScrollControlled: true, + context: context, + builder: (context) => ChatUserModal( + chatStore: chatStore, + username: ircMessage.user!, + userId: ircMessage.tags['user-id']!, + displayName: ircMessage.tags['display-name']!, + ), + ); + } + : null, + child: coloredMessage, + ); + }, + ); } } diff --git a/lib/screens/channel/chat/widgets/chat_user_modal.dart b/lib/screens/channel/chat/widgets/chat_user_modal.dart index 46164469..aeb90605 100644 --- a/lib/screens/channel/chat/widgets/chat_user_modal.dart +++ b/lib/screens/channel/chat/widgets/chat_user_modal.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/constants/constants.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_message.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; @@ -31,7 +32,6 @@ class ChatUserModal extends StatefulWidget { class _ChatUserModalState extends State { @override Widget build(BuildContext context) { - final userMessages = widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList(); final name = regexEnglish.hasMatch(widget.displayName) ? widget.displayName : '${widget.displayName} (${widget.username})'; return FrostyModal( @@ -90,48 +90,33 @@ class _ChatUserModalState extends State { 'Recent Messages', padding: EdgeInsets.all(10.0), ), - if (userMessages.isEmpty) - const Expanded(child: AlertMessage(message: 'No recent messages')) - else - Expanded( - child: MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale), - child: DefaultTextStyle( - style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize), - child: ListView.separated( - padding: EdgeInsets.zero, - reverse: true, - itemBuilder: (context, index) => InkWell( - onLongPress: () async { - HapticFeedback.lightImpact(); + Expanded( + child: Observer( + builder: (context) { + final userMessages = widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList(); - await Clipboard.setData(ClipboardData(text: userMessages[index].message)); + if (userMessages.isEmpty) { + return const AlertMessage(message: 'No recent messages'); + } - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: AlertMessage(message: 'Message copied to clipboard'), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: ChatMessage( + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale), + child: DefaultTextStyle( + style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize), + child: ListView.builder( + reverse: true, + itemCount: userMessages.length, + itemBuilder: (context, index) => ChatMessage( ircMessage: userMessages[index], - assetsStore: widget.chatStore.assetsStore, - settingsStore: widget.chatStore.settings, + chatStore: widget.chatStore, + isModal: true, ), ), - separatorBuilder: (context, index) => widget.chatStore.settings.showChatMessageDividers - ? Divider( - height: widget.chatStore.settings.messageSpacing, - thickness: 1.0, - ) - : SizedBox(height: widget.chatStore.settings.messageSpacing), - itemCount: userMessages.length, ), - ), - ), + ); + }, ), + ) ], ), ), diff --git a/lib/screens/settings/sections/chat_settings.dart b/lib/screens/settings/sections/chat_settings.dart index 53bfcce9..9827a778 100644 --- a/lib/screens/settings/sections/chat_settings.dart +++ b/lib/screens/settings/sections/chat_settings.dart @@ -21,7 +21,7 @@ class _ChatSettingsState extends State { @override Widget build(BuildContext context) { - const timestamps = ['Disabled', '12-Hour', '24-Hour']; + const timestamps = ['Disabled', '12-hour', '24-hour']; const sectionPadding = EdgeInsets.only(left: 16.0, bottom: 5.0, top: 30.0); @@ -96,6 +96,7 @@ class _ChatSettingsState extends State { ], ), subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Slider.adaptive( value: settingsStore.chatWidth,