From 108bd5573ef9b8e974b562c5ceaafe732b0233fd Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Mon, 3 Jan 2022 00:43:13 -0500 Subject: [PATCH] Add bold & highlighted user mentions in chat --- lib/models/irc_message.dart | 40 +++++++++++--------- lib/screens/channel/chat/chat_message.dart | 43 ++++++++++++++-------- lib/screens/channel/chat/chat_store.dart | 2 +- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/lib/models/irc_message.dart b/lib/models/irc_message.dart index 3079fa33..42e9afdb 100644 --- a/lib/models/irc_message.dart +++ b/lib/models/irc_message.dart @@ -15,6 +15,7 @@ class IRCMessage { final Map localEmotes; Command command; String? message; + bool mention; IRCMessage({ required this.raw, @@ -24,6 +25,7 @@ class IRCMessage { required this.user, required this.localEmotes, required this.message, + required this.mention, }); /// Returns a list of messages where the gievn CLEARCHAT message is applied. @@ -241,7 +243,7 @@ class IRCMessage { } // Italicize the text it was called with an IRC Action i.e., "/me". - final textStyle = action == true ? const TextStyle(fontStyle: FontStyle.italic) : null; + final textStyle = action == true ? const TextStyle(fontStyle: FontStyle.italic) : style; if (hideMessage) { span.add(const TextSpan(text: ' ')); @@ -317,7 +319,7 @@ class IRCMessage { if (nextEmote == null && !nextWordIsEmoji) { localSpan.add(const TextSpan(text: ' ')); - localSpan.add(TextSpan(text: words[index], style: textStyle)); + localSpan.add(_createTextSpan(text: words[index], style: textStyle)); } } else { localSpan.add( @@ -329,26 +331,18 @@ class IRCMessage { ); } } else { - localSpan.add(TextSpan(text: word, style: textStyle)); + localSpan.add(_createTextSpan(text: word, style: textStyle)); } localSpan.add(const TextSpan(text: ' ')); index--; } span.addAll(localSpan.reversed); } else { - // Use a string buffer to minimize TextSpan widgets. - // Instead of one TextSpan widget per word, we can have one TextSpan widget across multiple. - final buffer = StringBuffer(); - for (final word in words) { - buffer.write(' '); + span.add(const TextSpan(text: ' ')); final emote = emoteToObject[word] ?? localEmotes[word]; if (emote != null) { - span.add(TextSpan(text: buffer.toString(), style: textStyle)); - - buffer.clear(); - span.add( _createEmoteSpan( emoteUrl: emote.url, @@ -357,13 +351,9 @@ class IRCMessage { ), ); } else { - buffer.write(word); + span.add(_createTextSpan(text: word, style: textStyle)); } } - - if (buffer.isNotEmpty) { - span.add(TextSpan(text: buffer.toString(), style: textStyle)); - } } } } @@ -414,8 +404,16 @@ class IRCMessage { ); } + TextSpan _createTextSpan({required String text, TextStyle? style}) { + if (text.startsWith('@')) { + return TextSpan(text: text, style: style?.copyWith(fontWeight: FontWeight.bold)); + } else { + return TextSpan(text: text, style: style); + } + } + /// Parses an IRC string and returns its corresponding [IRCMessage] object. - factory IRCMessage.fromString(String whole) { + factory IRCMessage.fromString(String whole, {String? username}) { // We have three parts: // 1. The tags of the IRC message. // 2. The metadata (user, command, and channel). @@ -472,6 +470,10 @@ class IRCMessage { message = message.substring(8, message.length - 1); } + // Check if the message mentions the logged-in user + var mention = false; + if (message != null && username != null) mention = message.toLowerCase().contains(username); + // Now process any Twitch emotes contained in the message tags. // The map containing emotes from the user's tags to their URL. // This may include sub emotes that they can access but other users cannot. @@ -555,6 +557,7 @@ class IRCMessage { localEmotes: localEmotes, user: user, message: message, + mention: mention, ); } @@ -566,6 +569,7 @@ class IRCMessage { command: Command.notice, user: null, message: message, + mention: false, ); } diff --git a/lib/screens/channel/chat/chat_message.dart b/lib/screens/channel/chat/chat_message.dart index c482ee8c..36db4bac 100644 --- a/lib/screens/channel/chat/chat_message.dart +++ b/lib/screens/channel/chat/chat_message.dart @@ -27,24 +27,35 @@ class ChatMessage extends StatelessWidget { case Command.privateMessage: case Command.userState: // Render normal chat message (PRIVMSG). - return Padding( - padding: padding, - child: Text.rich( - TextSpan( - children: ircMessage.generateSpan( - style: DefaultTextStyle.of(context).style, - emoteToObject: assetsStore.emoteToObject, - twitchBadgeToObject: assetsStore.twitchBadgesToObject, - ffzUserToBadges: assetsStore.userToFFZBadges, - sevenTVUserToBadges: assetsStore.userTo7TVBadges, - bttvUserToBadge: assetsStore.userToBTTVBadges, - ffzRoomInfo: assetsStore.ffzRoomInfo, - zeroWidthEnabled: settingsStore.zeroWidthEnabled, - timestamp: timeStamps, - ), + final span = Text.rich( + TextSpan( + children: ircMessage.generateSpan( + style: DefaultTextStyle.of(context).style, + emoteToObject: assetsStore.emoteToObject, + twitchBadgeToObject: assetsStore.twitchBadgesToObject, + ffzUserToBadges: assetsStore.userToFFZBadges, + sevenTVUserToBadges: assetsStore.userTo7TVBadges, + bttvUserToBadge: assetsStore.userToBTTVBadges, + ffzRoomInfo: assetsStore.ffzRoomInfo, + zeroWidthEnabled: settingsStore.zeroWidthEnabled, + timestamp: timeStamps, ), ), ); + + if (ircMessage.mention) { + return Container( + padding: padding, + color: const Color(0x4DFF0000), + child: span, + ); + } else { + return Padding( + padding: padding, + child: span, + ); + } + case Command.clearChat: case Command.clearMessage: // Render timeouts and bans @@ -102,7 +113,7 @@ class ChatMessage extends StatelessWidget { case Command.userNotice: return Container( padding: padding, - color: const Color(0xFF673AB7).withOpacity(0.25), + color: const Color(0x339147FF), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/screens/channel/chat/chat_store.dart b/lib/screens/channel/chat/chat_store.dart index 1e2201c9..9e395424 100644 --- a/lib/screens/channel/chat/chat_store.dart +++ b/lib/screens/channel/chat/chat_store.dart @@ -141,7 +141,7 @@ abstract class _ChatStoreBase with Store { // To account for this, split by CRLF, then loop and process each message. for (final message in data.trimRight().split('\r\n')) { if (message.startsWith('@')) { - final parsedIRCMessage = IRCMessage.fromString(message); + final parsedIRCMessage = IRCMessage.fromString(message, username: auth.user.details?.login); // Filter messages from any blocked users if not a moderator or not the channel owner. if (!_userState.mod &&