diff --git a/lib/api/bttv_api.dart b/lib/api/bttv_api.dart index 91d2c6bc..9d54f5e2 100644 --- a/lib/api/bttv_api.dart +++ b/lib/api/bttv_api.dart @@ -41,7 +41,7 @@ class BTTV { } } - static Future?> getBadges() async { + static Future?> getBadges() async { final url = Uri.parse('https://api.betterttv.net/3/cached/badges'); final response = await http.get(url); @@ -50,7 +50,7 @@ class BTTV { final badgeObjects = badges.map((badge) => BadgeInfoBTTV.fromJson(badge)).toList(); - return {for (final badge in badgeObjects) badge.providerId: badge}; + return {for (final badge in badgeObjects) badge.providerId: Badge.fromBTTV(badge)}; } debugPrint('Failed to get FFZ badges'); } diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index 3384647d..783eac4e 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -46,7 +46,7 @@ class FFZ { } } - static Future>?> getBadges() async { + static Future>?> getBadges() async { final url = Uri.parse('https://api.frankerfacez.com/v1/badges/ids'); final response = await http.get(url); @@ -56,15 +56,16 @@ class FFZ { final badges = decoded['badges'] as List; final badgeObjects = badges.map((badge) => BadgeInfoFFZ.fromJson(badge)).toList(); - final result = >{}; + final result = >{}; for (final badge in badgeObjects.reversed) { for (final userId in decoded['users'][badge.id.toString()]) { final entry = result[userId.toString()]; + final normalBadge = Badge.fromFFZ(badge); if (entry == null) { - result[userId.toString()] = [badge]; + result[userId.toString()] = [normalBadge]; } else { - entry.add(badge); + entry.add(normalBadge); } } } diff --git a/lib/api/seventv_api.dart b/lib/api/seventv_api.dart index c86901fb..730be9db 100644 --- a/lib/api/seventv_api.dart +++ b/lib/api/seventv_api.dart @@ -38,7 +38,7 @@ class SevenTV { } } - static Future>?> getBadges() async { + static Future>?> getBadges() async { final url = Uri.parse('https://api.7tv.app/v2/badges?user_identifier=twitch_id'); final response = await http.get(url); @@ -46,14 +46,15 @@ class SevenTV { final decoded = jsonDecode(response.body)['badges'] as List; final badges = decoded.map((emote) => BadgeInfo7TV.fromJson(emote)); - final result = >{}; + final result = >{}; for (final badge in badges) { for (final userId in badge.users) { final entry = result[userId]; + final normalBadge = Badge.from7TV(badge); if (entry == null) { - result[userId] = [badge]; + result[userId] = [normalBadge]; } else { - entry.add(badge); + entry.add(normalBadge); } } } diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 81f08ef3..5910855e 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -69,17 +69,17 @@ 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; - decoded.forEach( - (id, versions) => (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo))); + decoded.forEach((id, versions) => + (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo)))); return result; } else { @@ -89,17 +89,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))); + decoded.forEach((id, versions) => + (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo)))); return result; } else { diff --git a/lib/models/badges.dart b/lib/models/badges.dart index 088a39d0..94ded0d2 100644 --- a/lib/models/badges.dart +++ b/lib/models/badges.dart @@ -123,3 +123,49 @@ class BadgeDetailsBTTV { factory BadgeDetailsBTTV.fromJson(Map json) => _$BadgeDetailsBTTVFromJson(json); } + +class Badge { + final String name; + final String url; + final BadgeType type; + final String? color; + + const Badge({ + required this.name, + required this.url, + this.color, + required this.type, + }); + + factory Badge.fromTwitch(BadgeInfoTwitch badge) => Badge( + name: badge.title, + url: badge.imageUrl4x, + type: BadgeType.twitch, + ); + + factory Badge.fromBTTV(BadgeInfoBTTV badge) => Badge( + name: badge.badge.description, + url: badge.badge.svg, + type: BadgeType.bttv, + ); + + factory Badge.fromFFZ(BadgeInfoFFZ badge) => Badge( + name: badge.title, + url: badge.urls.url4x, + color: badge.color, + type: BadgeType.ffz, + ); + + factory Badge.from7TV(BadgeInfo7TV badge) => Badge( + name: badge.tooltip, + url: badge.urls[2][1], + type: BadgeType.sevenTv, + ); +} + +enum BadgeType { + twitch, + bttv, + ffz, + sevenTv, +} diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index 075c349f..3d08f31a 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -258,7 +258,6 @@ class Emote7TV { /// The common emote class. class Emote { - final String id; final String name; final int? width; final int? height; @@ -267,7 +266,6 @@ class Emote { final EmoteType type; const Emote({ - required this.id, required this.name, this.width, this.height, @@ -277,7 +275,6 @@ class Emote { }); factory Emote.fromTwitch(EmoteTwitch emote, EmoteType type) => Emote( - id: emote.id, name: emote.name, zeroWidth: false, url: 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0', @@ -285,7 +282,6 @@ class Emote { ); factory Emote.fromBTTV(EmoteBTTV emote, EmoteType type) => Emote( - id: emote.id, name: emote.code, zeroWidth: zeroWidthEmotes.contains(emote.code), url: 'https://cdn.betterttv.net/emote/${emote.id}/3x', @@ -293,7 +289,6 @@ class Emote { ); factory Emote.fromFFZ(EmoteFFZ emote, EmoteType type) => Emote( - id: emote.id.toString(), name: emote.name, zeroWidth: false, url: 'https:' + (emote.urls.url4x ?? emote.urls.url2x ?? emote.urls.url1x), @@ -301,7 +296,6 @@ class Emote { ); factory Emote.from7TV(Emote7TV emote, EmoteType type) => Emote( - id: emote.id, name: emote.name, width: emote.width.first, height: emote.height.first, diff --git a/lib/models/irc_message.dart b/lib/models/irc_message.dart index 1e394b3f..2e808f22 100644 --- a/lib/models/irc_message.dart +++ b/lib/models/irc_message.dart @@ -129,16 +129,16 @@ class IRCMessage { for (final badge in twitchBadges) { final badgeInfo = twitchBadgeToObject[badge]; if (badgeInfo != null) { - var badgeUrl = badgeInfo.imageUrl4x; + var badgeUrl = badgeInfo.url; // Add custom FFZ mod badge if it exists. - if (badgeInfo.title == 'Moderator' && (ffzUserBadges != null || ffzRoomInfo?.modUrls != null)) { + if (badgeInfo.name == 'Moderator' && (ffzUserBadges != null || ffzRoomInfo?.modUrls != null)) { // Check if mod is bot. - final botBadge = ffzUserBadges?.firstWhereOrNull((element) => element.id == 2); + final botBadge = ffzUserBadges?.firstWhereOrNull((element) => element.name == 'Bot'); // Check if user has bot badge or room has custom FFZ mod badges if (botBadge != null) { - badgeUrl = 'https:' + botBadge.urls.url4x; + badgeUrl = 'https:' + botBadge.url; skipBot = true; } else if (ffzRoomInfo?.modUrls != null) { badgeUrl = 'https:' + (ffzRoomInfo!.modUrls?.url4x ?? ffzRoomInfo.modUrls?.url2x ?? ffzRoomInfo.modUrls!.url1x); @@ -157,14 +157,14 @@ class IRCMessage { } // Add custom FFZ vip badge if it exists - if (badgeInfo.title == 'VIP' && ffzRoomInfo?.vipBadge != null) { + if (badgeInfo.name == 'VIP' && ffzRoomInfo?.vipBadge != null) { badgeUrl = 'https:' + (ffzRoomInfo!.vipBadge?.url4x ?? ffzRoomInfo.vipBadge?.url2x ?? ffzRoomInfo.vipBadge!.url1x); } span.add( _createEmoteSpan( emoteUrl: badgeUrl, - tooltip: badgeInfo.title, + tooltip: badgeInfo.name, height: badgeHeight, ), ); @@ -176,15 +176,15 @@ class IRCMessage { // Add FFZ badges to span if (ffzUserBadges != null) { for (final badge in ffzUserBadges) { - if (badge.id == 2) { + if (badge.name == 'Bot') { if (!skipBot) { span.insert( 0, _createEmoteSpan( - emoteUrl: 'https:' + badge.urls.url4x, - tooltip: badge.title, + emoteUrl: 'https:' + badge.url, + tooltip: badge.name, height: badgeHeight, - backgroundColor: HexColor.fromHex(badge.color), + backgroundColor: HexColor.fromHex(badge.color!), ), ); span.add(const TextSpan(text: ' ')); @@ -192,10 +192,10 @@ class IRCMessage { } else { span.add( _createEmoteSpan( - emoteUrl: 'https:' + badge.urls.url4x, - tooltip: badge.title, + emoteUrl: 'https:' + badge.url, + tooltip: badge.name, height: badgeHeight, - backgroundColor: HexColor.fromHex(badge.color), + backgroundColor: HexColor.fromHex(badge.color!), ), ); span.add(const TextSpan(text: ' ')); @@ -208,8 +208,8 @@ class IRCMessage { if (userBTTVBadge != null) { span.add( _createEmoteSpan( - emoteUrl: userBTTVBadge.badge.svg, - tooltip: userBTTVBadge.badge.description, + emoteUrl: userBTTVBadge.url, + tooltip: userBTTVBadge.name, height: badgeHeight, isSvg: true, ), @@ -223,8 +223,8 @@ class IRCMessage { for (final badge in user7TVBadges) { span.add( _createEmoteSpan( - emoteUrl: badge.urls[2][1], - tooltip: badge.tooltip, + emoteUrl: badge.url, + tooltip: badge.name, height: badgeHeight, ), ); @@ -271,8 +271,28 @@ class IRCMessage { // Add the message and any emotes to the span. final chatMessage = message; if (chatMessage != null) { - final words = chatMessage.split(' '); - words.removeWhere((element) => element == ''); + final words = []; + final emojiBuffer = StringBuffer(); + final wordBuffer = StringBuffer(); + + for (final character in chatMessage.characters) { + if (regexEmoji.hasMatch(character)) { + if (wordBuffer.isNotEmpty) { + words.addAll(wordBuffer.toString().split(' ')); + wordBuffer.clear(); + } + emojiBuffer.write(character); + } else { + if (emojiBuffer.isNotEmpty) { + words.add(emojiBuffer.toString()); + emojiBuffer.clear(); + } + wordBuffer.write(character); + } + } + + if (wordBuffer.isNotEmpty) words.addAll(wordBuffer.toString().split(' ').where((element) => element != '')); + if (emojiBuffer.isNotEmpty) words.add(emojiBuffer.toString()); if (useZeroWidth) { final localSpan = []; @@ -492,7 +512,9 @@ class IRCMessage { // Also remove any "INVALID/UNDEFINED" Unicode characters. // Rendering this character on iOS shows a question mark inside a square. // This character is used by some clients to bypass restrictions on repeating message. - var message = splitMessage.length > 3 ? splitMessage.sublist(3).map((word) => word.replaceAll('\u{E0000}', '').trim()).join(' ').substring(1) : null; + var message = splitMessage.length > 3 + ? splitMessage.sublist(3).map((word) => word.replaceAll('\u{E0000}', '').trim()).where((element) => element != '').join(' ').substring(1) + : null; // Check if IRC actions like "/me" were called. var action = false; @@ -538,7 +560,6 @@ class IRCMessage { // Store the emote word and its associated URL for later use. localEmotes[emoteWord] = Emote( - id: emoteId, name: emoteWord, zeroWidth: false, url: 'https://static-cdn.jtvnw.net/emoticons/v2/$emoteId/default/dark/3.0', diff --git a/lib/screens/channel/stores/chat_assets_store.dart b/lib/screens/channel/stores/chat_assets_store.dart index 51064b38..cbaad9ab 100644 --- a/lib/screens/channel/stores/chat_assets_store.dart +++ b/lib/screens/channel/stores/chat_assets_store.dart @@ -25,7 +25,7 @@ abstract class _ChatAssetsStoreBase with Store { List get sevenTvEmotes => _emoteToObject.values.where((emote) => is7TV(emote)).toList(); /// The map of badges ids to their object representation. - final twitchBadgesToObject = ObservableMap(); + final twitchBadgesToObject = ObservableMap(); /// The map of emote words to their image or GIF URL. May be used by anyone in the chat. @readonly @@ -35,17 +35,17 @@ abstract class _ChatAssetsStoreBase with Store { @readonly var _userEmoteToObject = {}; - /// The map of usernames to their FFZ badges. + /// The map of user IDs to their FFZ badges. @readonly - var _userToFFZBadges = >{}; + var _userToFFZBadges = >{}; - /// The map of usernames to their 7TV badges. + /// The map of user IDs to their 7TV badges. @readonly - var _userTo7TVBadges = >{}; + var _userTo7TVBadges = >{}; - /// The map of usernames to their BTTV badge. + /// The map of user IDs to their BTTV badge. @readonly - var _userToBTTVBadges = {}; + var _userToBTTVBadges = {}; /// The current index of the emote menu stack. @observable diff --git a/lib/screens/channel/stores/chat_assets_store.g.dart b/lib/screens/channel/stores/chat_assets_store.g.dart index eaa98769..f9efa5b1 100644 --- a/lib/screens/channel/stores/chat_assets_store.g.dart +++ b/lib/screens/channel/stores/chat_assets_store.g.dart @@ -70,16 +70,16 @@ mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store { final _$_userToFFZBadgesAtom = Atom(name: '_ChatAssetsStoreBase._userToFFZBadges'); - Map> get userToFFZBadges { + Map> get userToFFZBadges { _$_userToFFZBadgesAtom.reportRead(); return super._userToFFZBadges; } @override - Map> get _userToFFZBadges => userToFFZBadges; + Map> get _userToFFZBadges => userToFFZBadges; @override - set _userToFFZBadges(Map> value) { + set _userToFFZBadges(Map> value) { _$_userToFFZBadgesAtom.reportWrite(value, super._userToFFZBadges, () { super._userToFFZBadges = value; }); @@ -88,16 +88,16 @@ mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store { final _$_userTo7TVBadgesAtom = Atom(name: '_ChatAssetsStoreBase._userTo7TVBadges'); - Map> get userTo7TVBadges { + Map> get userTo7TVBadges { _$_userTo7TVBadgesAtom.reportRead(); return super._userTo7TVBadges; } @override - Map> get _userTo7TVBadges => userTo7TVBadges; + Map> get _userTo7TVBadges => userTo7TVBadges; @override - set _userTo7TVBadges(Map> value) { + set _userTo7TVBadges(Map> value) { _$_userTo7TVBadgesAtom.reportWrite(value, super._userTo7TVBadges, () { super._userTo7TVBadges = value; }); @@ -106,16 +106,16 @@ mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store { final _$_userToBTTVBadgesAtom = Atom(name: '_ChatAssetsStoreBase._userToBTTVBadges'); - Map get userToBTTVBadges { + Map get userToBTTVBadges { _$_userToBTTVBadgesAtom.reportRead(); return super._userToBTTVBadges; } @override - Map get _userToBTTVBadges => userToBTTVBadges; + Map get _userToBTTVBadges => userToBTTVBadges; @override - set _userToBTTVBadges(Map value) { + set _userToBTTVBadges(Map value) { _$_userToBTTVBadgesAtom.reportWrite(value, super._userToBTTVBadges, () { super._userToBTTVBadges = value; });