diff --git a/lib/api/bttv_api.dart b/lib/api/bttv_api.dart index 9b9b76b3..5c514881 100644 --- a/lib/api/bttv_api.dart +++ b/lib/api/bttv_api.dart @@ -20,7 +20,7 @@ class BTTVApi { return emotes.map((emote) => Emote.fromBTTV(emote, EmoteType.bttvGlobal)).toList(); } else { - throw Exception('Failed to get global BTTV emotes.'); + return Future.error('Failed to get BTTV global emotes.'); } } @@ -39,7 +39,7 @@ class BTTVApi { return emoteToUrl; } else { - throw Exception('Failed to get channel BTTV emotes.'); + return Future.error('Failed to get BTTV channel emotes.'); } } @@ -54,7 +54,7 @@ class BTTVApi { return {for (final badge in badgeObjects) badge.providerId: Badge.fromBTTV(badge)}; } else { - throw Exception('Failed to get BTTV badges'); + return Future.error('Failed to get BTTV badges'); } } } diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index 26f83da6..d148b3e9 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -27,7 +27,7 @@ class FFZApi { return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList(); } else { - throw Exception('Failed to get global FFZ emotes.'); + return Future.error('Failed to get FFZ global emotes.'); } } @@ -45,7 +45,7 @@ class FFZApi { return Tuple2(roomInfo, emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList()); } else { - throw Exception('Failed to get FFZ room info.}'); + return Future.error('Failed to get FFZ room info.'); } } @@ -73,7 +73,7 @@ class FFZApi { return result; } else { - throw Exception('Failed to get FFZ badges.'); + return Future.error('Failed to get FFZ badges.'); } } } diff --git a/lib/api/seventv_api.dart b/lib/api/seventv_api.dart index 55eea2cc..8d60a243 100644 --- a/lib/api/seventv_api.dart +++ b/lib/api/seventv_api.dart @@ -21,7 +21,7 @@ class SevenTVAPI { return emotes.map((emote) => Emote.from7TV(emote, EmoteType.sevenTvGlobal)).toList(); } else { - throw Exception('Failed to get global 7TV emotes.'); + return Future.error('Failed to get 7TV global emotes'); } } @@ -36,7 +36,7 @@ class SevenTVAPI { return emotes.map((emote) => Emote.from7TV(emote, EmoteType.sevenTvChannel)).toList(); } else { - throw Exception('Failed to get channel 7TV emotes.'); + return Future.error('Failed to get 7TV channel emotes'); } } @@ -63,7 +63,7 @@ class SevenTVAPI { return result; } else { - throw Exception('Failed to get 7TV badges.'); + return Future.error('Failed to get 7TV badges.'); } } } diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 1b10ebeb..45aea8f8 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -26,7 +26,7 @@ class TwitchApi { return emotes.map((emote) => Emote.fromTwitch(emote, EmoteType.twitchGlobal)).toList(); } else { - throw Exception('Failed to get global Twitch emotes.'); + return Future.error('Failed to get Twitch global emotes.'); } } @@ -44,7 +44,7 @@ class TwitchApi { return emotes.map((emote) => Emote.fromTwitch(emote, EmoteType.twitchChannel)).toList(); } else { - throw Exception('Failed to get channel Twitch emotes.'); + return Future.error('Failed to get Twitch channel emotes.'); } } @@ -72,7 +72,7 @@ class TwitchApi { } }).toList(); } else { - throw Exception('Failed to get Twitch emotes set.'); + return Future.error('Failed to get Twitch emotes set.'); } } @@ -89,7 +89,7 @@ class TwitchApi { return result; } else { - throw Exception('Failed to get global Twitch badges.'); + return Future.error('Failed to get Twitch global badges.'); } } @@ -106,7 +106,7 @@ class TwitchApi { return result; } else { - throw Exception('Failed to get Twitch channel badges.'); + return Future.error('Failed to get Twitch channel badges.'); } } @@ -120,7 +120,7 @@ class TwitchApi { return UserTwitch.fromJson(userData.first); } else { - throw Exception('Failed to get user info'); + return Future.error('Failed to get Twitch user info'); } } @@ -141,7 +141,7 @@ class TwitchApi { if (response.statusCode == 200) { return jsonDecode(response.body)['access_token']; } else { - throw Exception('Failed to get default token.'); + return Future.error('Failed to get default token.'); } } @@ -227,10 +227,10 @@ class TwitchApi { if (data.isNotEmpty) { return StreamTwitch.fromJson(data.first); } else { - throw Exception('$userLogin is offline'); + return Future.error('$userLogin is offline'); } } else { - throw Exception('Failed to get stream info.'); + return Future.error('Failed to get stream info.'); } } @@ -293,7 +293,7 @@ class TwitchApi { return channelData.map((e) => ChannelQuery.fromJson(e)).toList(); } else { - throw Exception('Failed to get channels.'); + return Future.error('Failed to get channels.'); } } @@ -325,7 +325,7 @@ class TwitchApi { return CategoriesTwitch.fromJson(decoded); } else { - throw Exception('Failed to get categories.'); + return Future.error('Failed to get categories.'); } } @@ -339,7 +339,7 @@ class TwitchApi { return decoded['total'] as int; } else { - throw Exception('Failed to get sub count.'); + return Future.error('Failed to get sub count.'); } } @@ -352,7 +352,7 @@ class TwitchApi { return ChatUsers.fromJson(decoded); } else { - throw Exception('Failed to get chatters.'); + return Future.error('Failed to get chatters.'); } } @@ -381,10 +381,10 @@ class TwitchApi { return result; } else { - throw Exception('User does not have anyone blocked'); + return Future.error('User does not have anyone blocked'); } } else { - throw Exception('User does not exist'); + return Future.error('User does not exist'); } } diff --git a/lib/screens/channel/stores/chat_assets_store.dart b/lib/screens/channel/stores/chat_assets_store.dart index e7a0be16..2a05c3de 100644 --- a/lib/screens/channel/stores/chat_assets_store.dart +++ b/lib/screens/channel/stores/chat_assets_store.dart @@ -4,7 +4,6 @@ import 'package:frosty/api/seventv_api.dart'; import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; -import 'package:frosty/models/user.dart'; import 'package:mobx/mobx.dart'; part 'chat_assets_store.g.dart'; @@ -81,56 +80,76 @@ abstract class _ChatAssetsStoreBase with Store { /// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl] @action - Future getAssets({required String channelName, required Map headers}) async { - // Fetch the desired channel/user's information. - final channelInfo = await twitchApi.getUser(userLogin: channelName, headers: headers); - - if (channelInfo != null) { + Future assetsFuture({ + required String channelName, + required String channelId, + required Map headers, + required Function onEmoteError, + required Function onBadgeError, + }) => // Fetch the global and channel's assets (emotes & badges). // Async awaits are placed in a list so they are performed in parallel. - - await Future.wait([ - getEmotes(channelInfo: channelInfo, headers: headers), - getBadges(channelInfo: channelInfo, headers: headers), + Future.wait([ + emotesFuture( + channelName: channelName, + channelId: channelId, + headers: headers, + onError: onEmoteError, + ), + badgesFuture( + channelId: channelId, + headers: headers, + onError: onBadgeError, + ), ]); - } - } @action - Future getEmotes({required UserTwitch channelInfo, required Map headers}) async { - final assets = await Future.wait([ - ffzApi.getEmotesGlobal(), - bttvApi.getEmotesGlobal(), - bttvApi.getEmotesChannel(id: channelInfo.id), - twitchApi.getEmotesGlobal(headers: headers), - twitchApi.getEmotesChannel(id: channelInfo.id, headers: headers), - sevenTvApi.getEmotesGlobal(), - sevenTvApi.getEmotesChannel(user: channelInfo.login), - ffzApi.getRoomInfo(name: channelInfo.login).then((ffzRoom) { - ffzRoomInfo = ffzRoom?.item1; - return ffzRoom?.item2 ?? []; - }), - ]); - - final emotes = assets.expand((list) => list); - - _emoteToObject = {for (final emote in emotes) emote.name: emote}; - } + Future emotesFuture({ + required String channelId, + required String channelName, + required Map headers, + required Function onError, + }) => + // Parallel futures look ugly, but are worth the performance improvement. + // Each future has it's own catchError so that the entire future is not ended on an error. + // This will ensure that other futures complete even if one fails. + Future.wait([ + ffzApi.getEmotesGlobal().catchError(onError), + bttvApi.getEmotesGlobal().catchError(onError), + bttvApi.getEmotesChannel(id: channelId).catchError(onError), + twitchApi.getEmotesGlobal(headers: headers).catchError(onError), + twitchApi.getEmotesChannel(id: channelId, headers: headers).catchError(onError), + sevenTvApi.getEmotesGlobal().catchError(onError), + sevenTvApi.getEmotesChannel(user: channelName).catchError(onError), + ffzApi.getRoomInfo(name: channelName).then((ffzRoom) { + ffzRoomInfo = ffzRoom.item1; + return ffzRoom.item2; + }).catchError(onError), + ]).then((assets) => assets.expand((list) => list)).then((emotes) => _emoteToObject = {for (final emote in emotes) emote.name: emote}); @action - Future getBadges({required UserTwitch channelInfo, required Map headers}) async => await Future.wait([ + Future badgesFuture({ + required String channelId, + required Map headers, + required Function onError, + }) => + Future.wait([ twitchApi .getBadgesGlobal() .then((badges) => twitchBadgesToObject.addAll(badges)) - .then((_) => twitchApi.getBadgesChannel(id: channelInfo.id).then((badges) => twitchBadgesToObject.addAll(badges))), - ffzApi.getBadges().then((badges) => _userToFFZBadges = badges ?? {}), - sevenTvApi.getBadges().then((badges) => _userTo7TVBadges = badges ?? {}), - bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges ?? {}), + .then((_) => twitchApi.getBadgesChannel(id: channelId).then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError)), + ffzApi.getBadges().then((badges) => _userToFFZBadges = badges).catchError(onError), + sevenTvApi.getBadges().then((badges) => _userTo7TVBadges = badges).catchError(onError), + bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges).catchError(onError), ]); @action - Future getUserEmotes({required List emoteSets, required Map headers}) async => - await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers))) + Future userEmotesFuture({ + 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()); } diff --git a/lib/screens/channel/stores/chat_assets_store.g.dart b/lib/screens/channel/stores/chat_assets_store.g.dart index f9efa5b1..7db0dce5 100644 --- a/lib/screens/channel/stores/chat_assets_store.g.dart +++ b/lib/screens/channel/stores/chat_assets_store.g.dart @@ -152,41 +152,77 @@ mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store { }); } - final _$getAssetsAsyncAction = AsyncAction('_ChatAssetsStoreBase.getAssets'); + final _$_ChatAssetsStoreBaseActionController = + ActionController(name: '_ChatAssetsStoreBase'); @override - Future getAssets( - {required String channelName, required Map headers}) { - return _$getAssetsAsyncAction - .run(() => super.getAssets(channelName: channelName, headers: headers)); + Future assetsFuture( + {required String channelName, + required String channelId, + required Map headers, + required Function onEmoteError, + required Function onBadgeError}) { + final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction( + name: '_ChatAssetsStoreBase.assetsFuture'); + try { + return super.assetsFuture( + channelName: channelName, + channelId: channelId, + headers: headers, + onEmoteError: onEmoteError, + onBadgeError: onBadgeError); + } finally { + _$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo); + } } - final _$getEmotesAsyncAction = AsyncAction('_ChatAssetsStoreBase.getEmotes'); - @override - Future getEmotes( - {required UserTwitch channelInfo, required Map headers}) { - return _$getEmotesAsyncAction - .run(() => super.getEmotes(channelInfo: channelInfo, headers: headers)); + Future emotesFuture( + {required String channelId, + required String channelName, + required Map headers, + required Function onError}) { + final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction( + name: '_ChatAssetsStoreBase.emotesFuture'); + try { + return super.emotesFuture( + channelId: channelId, + channelName: channelName, + headers: headers, + onError: onError); + } finally { + _$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo); + } } - final _$getBadgesAsyncAction = AsyncAction('_ChatAssetsStoreBase.getBadges'); - @override - Future getBadges( - {required UserTwitch channelInfo, required Map headers}) { - return _$getBadgesAsyncAction - .run(() => super.getBadges(channelInfo: channelInfo, headers: headers)); + Future badgesFuture( + {required String channelId, + required Map headers, + required Function onError}) { + final _$actionInfo = _$_ChatAssetsStoreBaseActionController.startAction( + name: '_ChatAssetsStoreBase.badgesFuture'); + try { + return super.badgesFuture( + channelId: channelId, headers: headers, onError: onError); + } finally { + _$_ChatAssetsStoreBaseActionController.endAction(_$actionInfo); + } } - final _$getUserEmotesAsyncAction = - AsyncAction('_ChatAssetsStoreBase.getUserEmotes'); - @override - Future getUserEmotes( - {required List emoteSets, required Map headers}) { - return _$getUserEmotesAsyncAction - .run(() => super.getUserEmotes(emoteSets: emoteSets, headers: headers)); + 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 diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index f9211317..56365755 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:frosty/core/auth/auth_store.dart'; +import 'package:frosty/models/badges.dart'; +import 'package:frosty/models/emotes.dart'; import 'package:frosty/models/irc_message.dart'; import 'package:frosty/screens/channel/stores/chat_assets_store.dart'; import 'package:frosty/screens/channel/stores/chat_details_store.dart'; @@ -30,6 +32,12 @@ abstract class _ChatStoreBase with Store { /// The name of the channel to connect to. final String channelName; + /// The channel's ID for API requests. + final String channelId; + + /// The channel's display name to show on widgets. + final String displayName; + /// The Twitch IRC WebSocket channel. WebSocketChannel? _channel; @@ -74,6 +82,8 @@ abstract class _ChatStoreBase with Store { required this.assetsStore, required this.settings, required this.channelName, + required this.channelId, + required this.displayName, }) { // Create a reaction where anytime the emote menu is shown or hidden, // scroll to the bottom of the list. This will prevent the emote menu @@ -145,11 +155,14 @@ abstract class _ChatStoreBase with Store { case Command.globalUserState: final setIds = parsedIRCMessage.tags['emote-sets']?.split(','); if (setIds != null) { - _messages.add(IRCMessage.createNotice(message: 'Fetching user emotes...')); - assetsStore - .getUserEmotes(emoteSets: setIds, headers: auth.headersTwitch) - .then((_) => _messages.add(IRCMessage.createNotice(message: 'User emotes fetched!'))) - .onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch user emotes: ${error.toString()}'))); + assetsStore.userEmotesFuture( + emoteSets: setIds, + headers: auth.headersTwitch, + onError: (error) { + debugPrint(error.toString()); + return []; + }, + ); } continue; case Command.none: @@ -170,15 +183,22 @@ abstract class _ChatStoreBase with Store { _channel?.sink.add('PONG :tmi.twitch.tv'); return; } else if (message.contains('Welcome, GLHF!')) { - _messages.add(IRCMessage.createNotice(message: "Connected to $channelName's chat")); + _messages.add(IRCMessage.createNotice(message: "Connected to $displayName's chat")); // Fetch the assets used in chat including badges and emotes. - _messages.add(IRCMessage.createNotice(message: 'Fetching badges and emotes...')); - assetsStore - .getAssets(channelName: channelName, headers: auth.headersTwitch) - .then((_) => _messages.add(IRCMessage.createNotice(message: 'Badges and emotes fetched!'))) - .onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch assets: ${error.toString()}'))); - + assetsStore.assetsFuture( + channelName: channelName, + channelId: channelId, + headers: auth.headersTwitch, + onEmoteError: (error) { + debugPrint(error.toString()); + return []; + }, + onBadgeError: (error) { + debugPrint(error.toString()); + return []; + }, + ); // Reset exponential backoff if successfully connected. _retries = 0; _backoffTime = 0; @@ -211,7 +231,6 @@ abstract class _ChatStoreBase with Store { (data) => _handleIRCData(data.toString()), onError: (error) { debugPrint('Chat error: ${error.toString()}'); - _messages.add(IRCMessage.createNotice(message: 'Chat error - ${error.toString()}')); }, onDone: () async { if (_channel == null) return; diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index 2f0bbdb7..014fa066 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -35,6 +35,9 @@ class SearchResultsChannels extends StatelessWidget { builder: (context) => VideoChat( displayName: channelInfo.broadcasterName, chatStore: ChatStore( + channelName: channelInfo.broadcasterLogin, + channelId: channelInfo.broadcasterId, + displayName: channelInfo.broadcasterName, chatDetailsStore: ChatDetailsStore( twitchApi: context.read(), ), @@ -44,7 +47,6 @@ class SearchResultsChannels extends StatelessWidget { bttvApi: context.read(), sevenTvApi: context.read(), ), - channelName: channelInfo.broadcasterLogin, auth: searchStore.authStore, settings: context.read(), ), @@ -110,6 +112,9 @@ class SearchResultsChannels extends StatelessWidget { builder: (context) => VideoChat( displayName: channel.displayName, chatStore: ChatStore( + channelName: channel.broadcasterLogin, + channelId: channel.id, + displayName: channel.displayName, chatDetailsStore: ChatDetailsStore( twitchApi: context.read(), ), @@ -119,7 +124,6 @@ class SearchResultsChannels extends StatelessWidget { bttvApi: context.read(), sevenTvApi: context.read(), ), - channelName: channel.broadcasterLogin, auth: searchStore.authStore, settings: context.read(), ), diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index 55a97d2b..a19d25a6 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -44,6 +44,8 @@ class StreamCard extends StatelessWidget { displayName: streamInfo.userName, chatStore: ChatStore( channelName: streamInfo.userLogin, + channelId: streamInfo.userId, + displayName: streamInfo.userName, auth: context.read(), settings: context.read(), chatDetailsStore: ChatDetailsStore(