Add bold & highlighted user mentions in chat

This commit is contained in:
Tommy Chow 2022-01-03 00:43:13 -05:00
parent 0a729fbfa7
commit 108bd5573e
3 changed files with 50 additions and 35 deletions

View file

@ -15,6 +15,7 @@ class IRCMessage {
final Map<String, Emote> 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: ' <message deleted>'));
@ -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,
);
}

View file

@ -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: [

View file

@ -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 &&