diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index 546bebb2..027eb6cc 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -264,6 +264,7 @@ class Emote { final bool zeroWidth; final String url; final EmoteType type; + final String? ownerId; const Emote({ required this.name, @@ -272,6 +273,7 @@ class Emote { required this.zeroWidth, required this.url, required this.type, + this.ownerId, }); factory Emote.fromTwitch(EmoteTwitch emote, EmoteType type) => Emote( @@ -279,6 +281,7 @@ class Emote { zeroWidth: false, url: 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0', type: type, + ownerId: emote.ownerId, ); factory Emote.fromBTTV(EmoteBTTV emote, EmoteType type) => Emote( diff --git a/lib/models/emotes.g.dart b/lib/models/emotes.g.dart index 07e936f3..9cc8e12b 100644 --- a/lib/models/emotes.g.dart +++ b/lib/models/emotes.g.dart @@ -131,6 +131,7 @@ Emote _$EmoteFromJson(Map json) => Emote( zeroWidth: json['zeroWidth'] as bool, url: json['url'] as String, type: $enumDecode(_$EmoteTypeEnumMap, json['type']), + ownerId: json['ownerId'] as String?, ); Map _$EmoteToJson(Emote instance) => { @@ -140,6 +141,7 @@ Map _$EmoteToJson(Emote instance) => { 'zeroWidth': instance.zeroWidth, 'url': instance.url, 'type': _$EmoteTypeEnumMap[instance.type], + 'ownerId': instance.ownerId, }; const _$EmoteTypeEnumMap = { diff --git a/lib/screens/channel/chat/emote_menu/emote_menu.dart b/lib/screens/channel/chat/emote_menu/emote_menu.dart index 6f9a591c..739faf65 100644 --- a/lib/screens/channel/chat/emote_menu/emote_menu.dart +++ b/lib/screens/channel/chat/emote_menu/emote_menu.dart @@ -81,7 +81,7 @@ class _EmoteMenuState extends State { Observer( builder: (_) => EmoteMenuPanel( chatStore: widget.chatStore, - emotes: widget.chatStore.assetsStore.userEmoteToObject.values.toList(), + twitchEmotes: widget.chatStore.assetsStore.userEmoteSectionToEmotes, ), ), Observer( diff --git a/lib/screens/channel/chat/emote_menu/emote_menu_panel.dart b/lib/screens/channel/chat/emote_menu/emote_menu_panel.dart index 2b4625cb..a4dd9096 100644 --- a/lib/screens/channel/chat/emote_menu/emote_menu_panel.dart +++ b/lib/screens/channel/chat/emote_menu/emote_menu_panel.dart @@ -6,78 +6,72 @@ import 'package:frosty/widgets/section_header.dart'; class EmoteMenuPanel extends StatelessWidget { final ChatStore chatStore; - final List emotes; + final List? emotes; + final Map>? twitchEmotes; const EmoteMenuPanel({ Key? key, required this.chatStore, - required this.emotes, + this.emotes, + this.twitchEmotes, }) : super(key: key); @override Widget build(BuildContext context) { - final globalEmotes = emotes - .where((emote) => - emote.type == EmoteType.twitchGlobal || - emote.type == EmoteType.bttvGlobal || - emote.type == EmoteType.ffzGlobal || - emote.type == EmoteType.sevenTVGlobal) - .toList(); + if (emotes != null) { + final globalEmotes = + emotes!.where((emote) => emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.ffzGlobal || emote.type == EmoteType.sevenTVGlobal).toList(); - final channelEmotes = emotes - .where((emote) => - emote.type == EmoteType.twitchChannel || - emote.type == EmoteType.bttvChannel || - emote.type == EmoteType.bttvShared || - emote.type == EmoteType.ffzChannel || - emote.type == EmoteType.sevenTVChannel) - .toList(); + final channelEmotes = emotes! + .where((emote) => + emote.type == EmoteType.bttvChannel || + emote.type == EmoteType.bttvShared || + emote.type == EmoteType.ffzChannel || + emote.type == EmoteType.sevenTVChannel) + .toList(); - final subEmotes = emotes.where((emote) => emote.type == EmoteType.twitchSub).toList(); - final miscEmotes = emotes.where((emote) => emote.type == EmoteType.twitchUnlocked).toList(); - - return CustomScrollView( - slivers: [ - if (globalEmotes.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SectionHeader( - 'Global Emotes', - padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), + return CustomScrollView( + slivers: [ + if (globalEmotes.isNotEmpty) ...[ + const SliverToBoxAdapter( + child: SectionHeader( + 'Global Emotes', + padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), + ), ), - ), - EmoteMenuSection( - chatStore: chatStore, - emotes: globalEmotes, - ), + EmoteMenuSection( + chatStore: chatStore, + emotes: globalEmotes, + ), + ], + if (channelEmotes.isNotEmpty) ...[ + const SliverToBoxAdapter( + child: SectionHeader('Channel Emotes'), + ), + EmoteMenuSection( + chatStore: chatStore, + emotes: channelEmotes, + ), + ], ], - if (channelEmotes.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SectionHeader('Channel Emotes'), - ), - EmoteMenuSection( - chatStore: chatStore, - emotes: channelEmotes, - ), + ); + } else { + return CustomScrollView( + slivers: [ + for (final entry in twitchEmotes!.entries) ...[ + SliverToBoxAdapter( + child: SectionHeader( + entry.key, + padding: entry.key == 'Global Emotes' ? const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0) : null, + ), + ), + EmoteMenuSection( + chatStore: chatStore, + emotes: entry.value, + ), + ] ], - if (subEmotes.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SectionHeader('Subscribed Emotes'), - ), - EmoteMenuSection( - chatStore: chatStore, - emotes: subEmotes, - ), - ], - if (miscEmotes.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SectionHeader('Unlocked Emotes'), - ), - EmoteMenuSection( - chatStore: chatStore, - emotes: miscEmotes, - ), - ], - ], - ); + ); + } } } diff --git a/lib/screens/channel/stores/chat_assets_store.dart b/lib/screens/channel/stores/chat_assets_store.dart index fe51ba1b..0b0f52a5 100644 --- a/lib/screens/channel/stores/chat_assets_store.dart +++ b/lib/screens/channel/stores/chat_assets_store.dart @@ -43,8 +43,14 @@ abstract class ChatAssetsStoreBase with Store { /// The emotes that are "owned" and may be used by the current user. @readonly + // ignore: prefer_final_fields var _userEmoteToObject = {}; + /// The Twitch emote types mapped to their emotes (i.e., global, specific channels, unlocked). + @readonly + // ignore: prefer_final_fields + var _userEmoteSectionToEmotes = >{}; + /// The map of user IDs to their FFZ badges. @readonly var _userToFFZBadges = >{}; @@ -91,8 +97,8 @@ abstract class ChatAssetsStoreBase with Store { // Retrieve the instance that will allow us to retrieve local search history. final prefs = await SharedPreferences.getInstance(); - _recentEmotes = prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ?? - ObservableList(); + _recentEmotes = + prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ?? ObservableList(); _disposeReaction = autorun((_) { if (_recentEmotes.length > 48) _recentEmotes.removeLast(); @@ -167,10 +173,38 @@ abstract class ChatAssetsStoreBase with Store { required List emoteSets, required Map headers, required Function onError, - }) => - Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError))) - .then((emotes) => emotes.expand((list) => list).toList()) - .then((userEmotes) => _userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable()); + }) async { + final userEmotes = await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError))); + + for (final emoteSet in userEmotes) { + if (emoteSet.isNotEmpty) { + if (emoteSet.first.type == EmoteType.twitchSub) { + final owner = await twitchApi.getUser(id: emoteSet.first.ownerId, headers: headers); + _userEmoteSectionToEmotes.update( + owner.displayName, + (existingEmoteSet) => [...existingEmoteSet, ...emoteSet], + ifAbsent: () => emoteSet, + ); + } else if (emoteSet.first.type == EmoteType.twitchGlobal) { + _userEmoteSectionToEmotes.update( + 'Global Emotes', + (existingEmoteSet) => [...existingEmoteSet, ...emoteSet], + ifAbsent: () => emoteSet, + ); + } else if (emoteSet.first.type == EmoteType.twitchUnlocked) { + _userEmoteSectionToEmotes.update( + 'Unlocked Emotes', + (existingEmoteSet) => [...existingEmoteSet, ...emoteSet], + ifAbsent: () => emoteSet, + ); + } + + for (final emote in emoteSet) { + _userEmoteToObject[emote.name] = emote; + } + } + } + } void dispose() => _disposeReaction(); } diff --git a/lib/screens/channel/stores/chat_assets_store.g.dart b/lib/screens/channel/stores/chat_assets_store.g.dart index ae1c2b4a..9c9f7ab8 100644 --- a/lib/screens/channel/stores/chat_assets_store.g.dart +++ b/lib/screens/channel/stores/chat_assets_store.g.dart @@ -85,6 +85,26 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store { }); } + late final _$_userEmoteSectionToEmotesAtom = Atom( + name: 'ChatAssetsStoreBase._userEmoteSectionToEmotes', context: context); + + Map> get userEmoteSectionToEmotes { + _$_userEmoteSectionToEmotesAtom.reportRead(); + return super._userEmoteSectionToEmotes; + } + + @override + Map> get _userEmoteSectionToEmotes => + userEmoteSectionToEmotes; + + @override + set _userEmoteSectionToEmotes(Map> value) { + _$_userEmoteSectionToEmotesAtom + .reportWrite(value, super._userEmoteSectionToEmotes, () { + super._userEmoteSectionToEmotes = value; + }); + } + late final _$_userToFFZBadgesAtom = Atom(name: 'ChatAssetsStoreBase._userToFFZBadges', context: context); @@ -179,6 +199,18 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store { return _$initAsyncAction.run(() => super.init()); } + late final _$userEmotesFutureAsyncAction = + AsyncAction('ChatAssetsStoreBase.userEmotesFuture', context: context); + + @override + Future userEmotesFuture( + {required List emoteSets, + required Map headers, + required Function onError}) { + return _$userEmotesFutureAsyncAction.run(() => super.userEmotesFuture( + emoteSets: emoteSets, headers: headers, onError: onError)); + } + late final _$ChatAssetsStoreBaseActionController = ActionController(name: 'ChatAssetsStoreBase', context: context); @@ -231,21 +263,6 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store { } } - @override - Future userEmotesFuture( - {required List emoteSets, - required Map headers, - required Function onError}) { - final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction( - name: 'ChatAssetsStoreBase.userEmotesFuture'); - try { - return super.userEmotesFuture( - emoteSets: emoteSets, headers: headers, onError: onError); - } finally { - _$ChatAssetsStoreBaseActionController.endAction(_$actionInfo); - } - } - @override String toString() { return ''' diff --git a/lib/widgets/section_header.dart b/lib/widgets/section_header.dart index b2c6baef..f47c54d9 100644 --- a/lib/widgets/section_header.dart +++ b/lib/widgets/section_header.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; class SectionHeader extends StatelessWidget { final String text; - final EdgeInsets padding; + final EdgeInsets? padding; final double? fontSize; final bool showDivider; const SectionHeader( this.text, { Key? key, - this.padding = const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0), + this.padding, this.fontSize, this.showDivider = false, }) : super(key: key); @@ -17,7 +17,7 @@ class SectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: padding, + padding: padding ?? const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [