- Fix cold-start privacy bug: apply persisted collection preference on launch - Disable Firebase collection in debug builds to avoid polluting prod data - Add Crashlytics custom keys (is_logged_in, channel), user identifier, and WebSocket reconnection breadcrumbs for actionable crash reports - Record non-fatal errors for server failures, WebSocket errors, and global asset fetch failures (skip timeouts/network errors to avoid noise) - Update is_logged_in key and user identifier on login/logout transitions - Log auth reconnection exhaustion before forced logout - Add FirebaseAnalyticsObserver with named routes for automatic screen tracking - Add static routeName constants to avoid duplicated string literals - Add Firebase Performance Dio interceptor for automatic HTTP request tracing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
253 lines
7.3 KiB
Dart
253 lines
7.3 KiB
Dart
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:frosty/apis/bttv_api.dart';
|
|
import 'package:frosty/apis/ffz_api.dart';
|
|
import 'package:frosty/apis/seventv_api.dart';
|
|
import 'package:frosty/apis/twitch_api.dart';
|
|
import 'package:frosty/models/badges.dart';
|
|
import 'package:frosty/models/emotes.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
|
|
part 'global_assets_store.g.dart';
|
|
|
|
/// Singleton-like store for caching global emotes and badges.
|
|
/// Provided at app root level via Provider, shared across all chat tabs.
|
|
class GlobalAssetsStore = GlobalAssetsStoreBase with _$GlobalAssetsStore;
|
|
|
|
abstract class GlobalAssetsStoreBase with Store {
|
|
final TwitchApi twitchApi;
|
|
final BTTVApi bttvApi;
|
|
final FFZApi ffzApi;
|
|
final SevenTVApi sevenTVApi;
|
|
|
|
GlobalAssetsStoreBase({
|
|
required this.twitchApi,
|
|
required this.bttvApi,
|
|
required this.ffzApi,
|
|
required this.sevenTVApi,
|
|
});
|
|
|
|
// ============= Loading State =============
|
|
|
|
/// Whether global assets have been loaded at least once.
|
|
@readonly
|
|
var _isLoaded = false;
|
|
|
|
/// Whether global assets are currently being fetched.
|
|
@readonly
|
|
var _isLoading = false;
|
|
|
|
/// Completer to allow multiple callers to await the same load operation.
|
|
Future<void>? _loadingFuture;
|
|
|
|
// ============= Global Emotes =============
|
|
|
|
/// Global Twitch emotes.
|
|
@readonly
|
|
var _twitchGlobalEmotes = <Emote>[];
|
|
|
|
/// Global 7TV emotes.
|
|
@readonly
|
|
var _sevenTVGlobalEmotes = <Emote>[];
|
|
|
|
/// Global BTTV emotes.
|
|
@readonly
|
|
var _bttvGlobalEmotes = <Emote>[];
|
|
|
|
/// Global FFZ emotes.
|
|
@readonly
|
|
var _ffzGlobalEmotes = <Emote>[];
|
|
|
|
// ============= Global Badges =============
|
|
|
|
/// Global Twitch badges (badge key -> ChatBadge).
|
|
@readonly
|
|
var _twitchGlobalBadges = <String, ChatBadge>{};
|
|
|
|
/// BTTV badges (providerId -> ChatBadge).
|
|
@readonly
|
|
var _bttvBadges = <String, ChatBadge>{};
|
|
|
|
/// FFZ badges (userId to list of ChatBadge).
|
|
@readonly
|
|
var _ffzBadges = <String, List<ChatBadge>>{};
|
|
|
|
// ============= Computed Properties =============
|
|
|
|
/// All global emotes combined into a single map (name -> Emote).
|
|
@computed
|
|
Map<String, Emote> get globalEmoteMap {
|
|
final result = <String, Emote>{};
|
|
for (final emote in _twitchGlobalEmotes) {
|
|
result[emote.name] = emote;
|
|
}
|
|
for (final emote in _sevenTVGlobalEmotes) {
|
|
result[emote.name] = emote;
|
|
}
|
|
for (final emote in _bttvGlobalEmotes) {
|
|
result[emote.name] = emote;
|
|
}
|
|
for (final emote in _ffzGlobalEmotes) {
|
|
result[emote.name] = emote;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ============= Methods =============
|
|
|
|
/// Ensures global assets are loaded. Safe to call multiple times.
|
|
/// Returns immediately if already loaded, or waits for in-progress load.
|
|
@action
|
|
Future<void> ensureLoaded({
|
|
bool showTwitchEmotes = true,
|
|
bool showTwitchBadges = true,
|
|
bool show7TVEmotes = true,
|
|
bool showBTTVEmotes = true,
|
|
bool showBTTVBadges = true,
|
|
bool showFFZEmotes = true,
|
|
bool showFFZBadges = true,
|
|
}) async {
|
|
// Already loaded - return immediately
|
|
if (_isLoaded) return;
|
|
|
|
// Currently loading - wait for existing operation
|
|
if (_isLoading && _loadingFuture != null) {
|
|
await _loadingFuture;
|
|
return;
|
|
}
|
|
|
|
// Start new load operation
|
|
_isLoading = true;
|
|
_loadingFuture = _fetchGlobalAssets(
|
|
showTwitchEmotes: showTwitchEmotes,
|
|
showTwitchBadges: showTwitchBadges,
|
|
show7TVEmotes: show7TVEmotes,
|
|
showBTTVEmotes: showBTTVEmotes,
|
|
showBTTVBadges: showBTTVBadges,
|
|
showFFZEmotes: showFFZEmotes,
|
|
showFFZBadges: showFFZBadges,
|
|
);
|
|
|
|
await _loadingFuture;
|
|
_isLoading = false;
|
|
_isLoaded = true;
|
|
_loadingFuture = null;
|
|
}
|
|
|
|
/// Force refresh global assets (e.g., when settings change).
|
|
@action
|
|
Future<void> refresh({
|
|
bool showTwitchEmotes = true,
|
|
bool showTwitchBadges = true,
|
|
bool show7TVEmotes = true,
|
|
bool showBTTVEmotes = true,
|
|
bool showBTTVBadges = true,
|
|
bool showFFZEmotes = true,
|
|
bool showFFZBadges = true,
|
|
}) async {
|
|
_isLoaded = false;
|
|
await ensureLoaded(
|
|
showTwitchEmotes: showTwitchEmotes,
|
|
showTwitchBadges: showTwitchBadges,
|
|
show7TVEmotes: show7TVEmotes,
|
|
showBTTVEmotes: showBTTVEmotes,
|
|
showBTTVBadges: showBTTVBadges,
|
|
showFFZEmotes: showFFZEmotes,
|
|
showFFZBadges: showFFZBadges,
|
|
);
|
|
}
|
|
|
|
@action
|
|
Future<void> _fetchGlobalAssets({
|
|
required bool showTwitchEmotes,
|
|
required bool showTwitchBadges,
|
|
required bool show7TVEmotes,
|
|
required bool showBTTVEmotes,
|
|
required bool showBTTVBadges,
|
|
required bool showFFZEmotes,
|
|
required bool showFFZBadges,
|
|
}) async {
|
|
void onAssetError(dynamic error, StackTrace stack) {
|
|
debugPrint('GlobalAssetsStore error: $error');
|
|
FirebaseCrashlytics.instance.recordError(
|
|
error,
|
|
stack,
|
|
reason: 'Global asset fetch failed',
|
|
);
|
|
}
|
|
|
|
await Future.wait([
|
|
// Twitch global emotes
|
|
if (showTwitchEmotes)
|
|
twitchApi
|
|
.getEmotesGlobal()
|
|
.then((emotes) => _twitchGlobalEmotes = emotes)
|
|
.catchError((e, StackTrace stack) {
|
|
onAssetError(e, stack);
|
|
_twitchGlobalEmotes = <Emote>[];
|
|
return _twitchGlobalEmotes;
|
|
}),
|
|
// 7TV global emotes
|
|
if (show7TVEmotes)
|
|
sevenTVApi
|
|
.getEmotesGlobal()
|
|
.then((emotes) => _sevenTVGlobalEmotes = emotes)
|
|
.catchError((e, StackTrace stack) {
|
|
onAssetError(e, stack);
|
|
_sevenTVGlobalEmotes = <Emote>[];
|
|
return _sevenTVGlobalEmotes;
|
|
}),
|
|
// BTTV global emotes
|
|
if (showBTTVEmotes)
|
|
bttvApi
|
|
.getEmotesGlobal()
|
|
.then((emotes) => _bttvGlobalEmotes = emotes)
|
|
.catchError((e, StackTrace stack) {
|
|
onAssetError(e, stack);
|
|
_bttvGlobalEmotes = <Emote>[];
|
|
return _bttvGlobalEmotes;
|
|
}),
|
|
// FFZ global emotes
|
|
if (showFFZEmotes)
|
|
ffzApi
|
|
.getEmotesGlobal()
|
|
.then((emotes) => _ffzGlobalEmotes = emotes)
|
|
.catchError((e, StackTrace stack) {
|
|
onAssetError(e, stack);
|
|
_ffzGlobalEmotes = <Emote>[];
|
|
return _ffzGlobalEmotes;
|
|
}),
|
|
|
|
// Twitch global badges
|
|
if (showTwitchBadges)
|
|
twitchApi
|
|
.getBadgesGlobal()
|
|
.then((badges) => _twitchGlobalBadges = badges)
|
|
.catchError((e, StackTrace stack) {
|
|
onAssetError(e, stack);
|
|
_twitchGlobalBadges = <String, ChatBadge>{};
|
|
return _twitchGlobalBadges;
|
|
}),
|
|
// BTTV badges (global - provider ID to badge mapping)
|
|
if (showBTTVBadges)
|
|
bttvApi.getBadges().then((badges) => _bttvBadges = badges).catchError((
|
|
e,
|
|
StackTrace stack,
|
|
) {
|
|
onAssetError(e, stack);
|
|
_bttvBadges = <String, ChatBadge>{};
|
|
return _bttvBadges;
|
|
}),
|
|
// FFZ badges (global - user ID to badges mapping)
|
|
if (showFFZBadges)
|
|
ffzApi.getBadges().then((badges) => _ffzBadges = badges).catchError((
|
|
e,
|
|
StackTrace stack,
|
|
) {
|
|
onAssetError(e, stack);
|
|
_ffzBadges = <String, List<ChatBadge>>{};
|
|
return _ffzBadges;
|
|
}),
|
|
]);
|
|
}
|
|
}
|