From 0457d3e85e7bb280a2e3759f871b6af2746b1710 Mon Sep 17 00:00:00 2001 From: tommyxchow Date: Sat, 11 Dec 2021 17:09:15 -0500 Subject: [PATCH] Add proper tooltips for badges --- lib/api/irc_api.dart | 18 +++++--- lib/api/twitch_api.dart | 16 ++++---- lib/screens/channel/chat/chat_store.dart | 52 ++++++++++++++++++------ 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/lib/api/irc_api.dart b/lib/api/irc_api.dart index ab316b63..693d55a6 100644 --- a/lib/api/irc_api.dart +++ b/lib/api/irc_api.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:frosty/models/badges.dart'; import 'package:frosty/models/irc.dart'; class IRC { @@ -49,7 +50,12 @@ class IRC { } /// Returns an [InlineSpan] list that corresponds to the badges, username, words, and emotes of the given [IRCMessage]. - static List generateSpan({required IRCMessage ircMessage, required Map assetToUrl, bool hideMessage = false}) { + static List generateSpan({ + required IRCMessage ircMessage, + required Map emoteToUrl, + required Map badgeToObject, + bool hideMessage = false, + }) { // The span list that will be used to render the chat message final span = []; @@ -92,16 +98,16 @@ class IRC { final badges = ircMessage.tags['badges']; if (badges != null) { for (final badge in badges.split(',')) { - final badgeUrl = assetToUrl[badge]; - if (badgeUrl != null) { + final badgeInfo = badgeToObject[badge]; + if (badgeInfo != null) { span.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: Tooltip( - message: badge, + message: badgeInfo.title, preferBelow: false, child: CachedNetworkImage( - imageUrl: badgeUrl, + imageUrl: badgeInfo.imageUrl4x, placeholder: (context, url) => const SizedBox(), fadeInDuration: const Duration(seconds: 0), height: 20, @@ -148,7 +154,7 @@ class IRC { for (final word in words) { buffer.write(' '); - final emoteUrl = assetToUrl[word] ?? localEmoteToUrl[word]; + final emoteUrl = emoteToUrl[word] ?? localEmoteToUrl[word]; if (emoteUrl != null) { span.add(TextSpan(text: buffer.toString())); diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 6b674de5..a029077d 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -51,18 +51,18 @@ class Twitch { } /// Returns a map of global Twitch badges to their URL. - static Future?> getBadgesGlobal() async { + static Future?> getBadgesGlobal() async { final url = Uri.parse('https://badges.twitch.tv/v1/badges/global/display'); final response = await http.get(url); if (response.statusCode == 200) { - final result = {}; + final result = {}; final decoded = jsonDecode(response.body)['badge_sets'] as Map; // TODO: Figure out cleaner way to decode badge JSON. - decoded.forEach((id, versions) => - (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo).imageUrl4x)); + decoded.forEach( + (id, versions) => (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo))); return result; } else { @@ -71,17 +71,17 @@ class Twitch { } /// Returns a map of a channel's Twitch badges to their URL. - static Future?> getBadgesChannel({required String id}) async { + static Future?> getBadgesChannel({required String id}) async { final url = Uri.parse('https://badges.twitch.tv/v1/badges/channels/$id/display'); final response = await http.get(url); if (response.statusCode == 200) { - final result = {}; + final result = {}; final decoded = jsonDecode(response.body)['badge_sets'] as Map; - decoded.forEach((id, versions) => - (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo).imageUrl4x)); + decoded.forEach( + (id, versions) => (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo))); return result; } else { diff --git a/lib/screens/channel/chat/chat_store.dart b/lib/screens/channel/chat/chat_store.dart index 4c511ae6..8bb6afe8 100644 --- a/lib/screens/channel/chat/chat_store.dart +++ b/lib/screens/channel/chat/chat_store.dart @@ -7,6 +7,7 @@ import 'package:frosty/api/seventv_api.dart'; import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/core/settings/settings_store.dart'; +import 'package:frosty/models/badges.dart'; import 'package:frosty/models/irc.dart'; import 'package:mobx/mobx.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -20,8 +21,11 @@ abstract class _ChatStoreBase with Store { /// The Twitch IRC WebSocket channel. final _channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443')); - /// The map of badges and emote words to their image or GIF URL. - final _assetToUrl = {}; + /// The map of emote words to their image or GIF URL. + final _emoteToUrl = {}; + + /// The map of badges ids to their object representation. + final _badgesToObject = {}; /// The list of chat messages to render and display. final messages = ObservableList(); @@ -213,7 +217,7 @@ abstract class _ChatStoreBase with Store { textController.clear(); } - /// Fetches global and channel assets (badges and emotes) and stores them in [_assetToUrl] + /// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl] Future getAssets() async { // Fetch the desired channel/user's information. final channelInfo = await Twitch.getUser(userLogin: channelName, headers: auth.headersTwitch); @@ -228,17 +232,24 @@ abstract class _ChatStoreBase with Store { await BTTV.getEmotesChannel(id: channelInfo.id), await Twitch.getEmotesGlobal(headers: auth.headersTwitch), await Twitch.getEmotesChannel(id: channelInfo.id, headers: auth.headersTwitch), - await Twitch.getBadgesGlobal(), - await Twitch.getBadgesChannel(id: channelInfo.id), await SevenTV.getEmotesGlobal(), await SevenTV.getEmotesChannel(user: channelInfo.login) ]; - // Add all the assets to the global word-to-asset map. - // 'Global' meaning these assets can be used by any message when rendering. for (final map in assets) { if (map != null) { - _assetToUrl.addAll(map); + _emoteToUrl.addAll(map); + } + } + + final badges = [ + await Twitch.getBadgesGlobal(), + await Twitch.getBadgesChannel(id: channelInfo.id), + ]; + + for (final map in badges) { + if (map != null) { + _badgesToObject.addAll(map); } } } @@ -251,9 +262,18 @@ abstract class _ChatStoreBase with Store { final List span; if (settings.hideBannedMessages) { - span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl, hideMessage: true); + span = IRC.generateSpan( + ircMessage: ircMessage, + emoteToUrl: _emoteToUrl, + badgeToObject: _badgesToObject, + hideMessage: true, + ); } else { - span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl); + span = IRC.generateSpan( + ircMessage: ircMessage, + badgeToObject: _badgesToObject, + emoteToUrl: _emoteToUrl, + ); } // Render timeouts and bans @@ -298,7 +318,11 @@ abstract class _ChatStoreBase with Store { ), ); } else if (ircMessage.command == Command.userNotice) { - final span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl); + final span = IRC.generateSpan( + ircMessage: ircMessage, + emoteToUrl: _emoteToUrl, + badgeToObject: _badgesToObject, + ); // Render sub alerts return Container( @@ -319,7 +343,11 @@ abstract class _ChatStoreBase with Store { ), ); } else { - final span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl); + final span = IRC.generateSpan( + ircMessage: ircMessage, + emoteToUrl: _emoteToUrl, + badgeToObject: _badgesToObject, + ); // Render normal chat message (PRIVMSG). return Padding(