diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index dfe0c66f03..e85bc03221 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -7,8 +7,6 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; import 'package:logging/logging.dart'; @@ -45,18 +43,6 @@ Future> accountActivity(AccountActivityRef ref) async { ); } -@riverpod -Future> accountRecentGames( - AccountRecentGamesRef ref, -) async { - final session = ref.watch(authSessionProvider); - if (session == null) return IList(); - return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(session.user.id), - const Duration(hours: 1), - ); -} - @riverpod Future> ongoingGames(OngoingGamesRef ref) async { final session = ref.watch(authSessionProvider); @@ -71,7 +57,7 @@ Future> ongoingGames(OngoingGamesRef ref) async { class AccountRepository { AccountRepository(this.client); - final http.Client client; + final LichessClient client; final Logger _log = Logger('AccountRepository'); Future getProfile() { diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 2b316d0d7d..3311f11a40 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -21,12 +21,12 @@ FlutterAppAuth appAuth(AppAuthRef ref) { class AuthRepository { AuthRepository( - http.Client client, + LichessClient client, FlutterAppAuth appAuth, ) : _client = client, _appAuth = appAuth; - final http.Client _client; + final LichessClient _client; final Logger _log = Logger('AuthRepository'); final FlutterAppAuth _appAuth; diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index ec0f698bf0..ef8ad79175 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,11 +1,12 @@ import 'package:http/http.dart' as http; +import 'package:lichess_mobile/src/model/common/http.dart'; import './challenge_request.dart'; class ChallengeRepository { const ChallengeRepository(this.client); - final http.Client client; + final LichessClient client; Future challenge(String username, ChallengeRequest req) async { final uri = Uri(path: '/api/challenge/$username'); diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index ae15124fdf..7341e384d3 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -72,7 +72,10 @@ class ClientEval with _$ClientEval implements Eval { } IList get bestMoves { - return pvs.map((e) => Move.fromUci(e.moves.first)).toIList(); + return pvs + .where((e) => e.moves.isNotEmpty) + .map((e) => Move.fromUci(e.moves.first)) + .toIList(); } @override diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 6c15dc7747..5b510ce38f 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -81,7 +81,7 @@ Client defaultClient(DefaultClientRef ref) { /// /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -Client lichessClient(LichessClientRef ref) { +LichessClient lichessClient(LichessClientRef ref) { final client = LichessClient( // Retry just once, after 500ms, on 429 Too Many Requests. RetryClient( @@ -178,8 +178,6 @@ class LichessClient implements Client { final request = response.request!; final method = request.method; final url = request.url; - // TODD for now logging isn't much useful - // We could use improve it later to create an http logger in the app. _logger.warning( '$method $url responded with status ${response.statusCode} ${response.reasonPhrase}', ); @@ -284,16 +282,16 @@ class LichessClient implements Client { return Response.fromStream(await send(request)); } +} - /// Throws an error if [response] is not successful. - void _checkResponseSuccess(Uri url, Response response) { - if (response.statusCode < 400) return; - var message = 'Request to $url failed with status ${response.statusCode}'; - if (response.reasonPhrase != null) { - message = '$message: ${response.reasonPhrase}'; - } - throw ClientException('$message.', url); +/// Throws an error if [response] is not successful. +void _checkResponseSuccess(Uri url, Response response) { + if (response.statusCode < 400) return; + var message = 'Request to $url failed with status ${response.statusCode}'; + if (response.reasonPhrase != null) { + message = '$message: ${response.reasonPhrase}'; } + throw ClientException('$message.', url); } extension ClientExtension on Client { @@ -464,21 +462,11 @@ extension ClientExtension on Client { ); } } - - /// Throws an error if [response] is not successful. - void _checkResponseSuccess(Uri url, Response response) { - if (response.statusCode < 400) return; - var message = 'Request to $url failed with status ${response.statusCode}'; - if (response.reasonPhrase != null) { - message = '$message: ${response.reasonPhrase}'; - } - throw ClientException('$message.', url); - } } extension ClientWidgetRefExtension on WidgetRef { /// Runs [fn] with a [LichessClient]. - Future withClient(Future Function(Client) fn) async { + Future withClient(Future Function(LichessClient) fn) async { final client = read(lichessClientProvider); return await fn(client); } @@ -486,7 +474,7 @@ extension ClientWidgetRefExtension on WidgetRef { extension ClientRefExtension on Ref { /// Runs [fn] with a [LichessClient]. - Future withClient(Future Function(Client) fn) async { + Future withClient(Future Function(LichessClient) fn) async { final client = read(lichessClientProvider); return await fn(client); } @@ -500,7 +488,7 @@ extension ClientAutoDisposeRefExtension on AutoDisposeRef { /// If [fn] throws with a [SocketException], the provider is not kept alive, this /// allows to retry the request later. Future withClientCacheFor( - Future Function(Client) fn, + Future Function(LichessClient) fn, Duration duration, ) async { final link = keepAlive(); diff --git a/lib/src/model/engine/uci_protocol.dart b/lib/src/model/engine/uci_protocol.dart index 4292f8a077..549867eeb4 100644 --- a/lib/src/model/engine/uci_protocol.dart +++ b/lib/src/model/engine/uci_protocol.dart @@ -99,9 +99,9 @@ class UCIProtocol { _stopRequested != true && parts.first == 'info') { int depth = 0; - int? nodes; + int nodes = 0; int multiPv = 1; - int? elapsedMs; + int elapsedMs = 0; String? evalType; bool isMate = false; int? povEv; @@ -130,16 +130,10 @@ class UCIProtocol { } } - // Sometimes we get #0. Let's just skip it. - if (isMate && povEv == 0) return; - // Track max pv index to determine when pv prints are done. if (_expectedPvs < multiPv) _expectedPvs = multiPv; - if (depth < minDepth || - nodes == null || - elapsedMs == null || - povEv == null) return; + if ((depth < minDepth && moves.isNotEmpty) || povEv == null) return; final pivot = _work!.threatMode == true ? 0 : 1; final ev = _work!.ply % 2 == pivot ? -povEv : povEv; diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 4e01bdf8c8..99ca23a623 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -85,6 +85,9 @@ class ArchivedGame : white; } +/// A [LightArchivedGame] associated with a point of view of a player. +typedef LightArchivedGameWithPov = ({LightArchivedGame game, Side pov}); + /// A lichess game exported from the API, with less data than [ArchivedGame]. /// /// This is commonly used to display a list of games. diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart new file mode 100644 index 0000000000..3aa5cbeaa2 --- /dev/null +++ b/lib/src/model/game/game_history.dart @@ -0,0 +1,214 @@ +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; +import 'package:result_extensions/result_extensions.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_history.freezed.dart'; +part 'game_history.g.dart'; + +const kNumberOfRecentGames = 20; + +const _nbPerPage = 20; + +/// A provider that fetches the current app user's recent games. +/// +/// If the user is logged in, the recent games are fetched from the server. +/// If the user is not logged in, or there is no connectivity, the recent games +/// stored locally are fetched instead. +@riverpod +Future> myRecentGames(MyRecentGamesRef ref) { + final connectivity = ref.watch(connectivityProvider); + final session = ref.watch(authSessionProvider); + final online = connectivity.valueOrNull?.isOnline ?? false; + if (session != null && online) { + return ref.withClientCacheFor( + (client) => GameRepository(client) + .getUserGames(session.user.id, max: kNumberOfRecentGames), + const Duration(hours: 1), + ); + } else { + final storage = ref.watch(gameStorageProvider); + ref.cacheFor(const Duration(hours: 1)); + return storage + .page(userId: session?.user.id, max: kNumberOfRecentGames) + .then( + (value) => value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), + ); + } +} + +/// A provider that fetches the recent games from the server for a given user. +@riverpod +Future> userRecentGames( + UserRecentGamesRef ref, { + required UserId userId, +}) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getUserGames(userId), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} + +/// A provider that fetches the total number of games played by given user, or the current app user if no user is provided. +/// +/// If the user is logged in, the number of games is fetched from the server. +/// If the user is not logged in, or there is no connectivity, the number of games +/// stored locally are fetched instead. +@riverpod +Future userNumberOfGames( + UserNumberOfGamesRef ref, + LightUser? user, { + required bool isOnline, +}) async { + final session = ref.watch(authSessionProvider); + return user != null + ? ref.watch( + userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), + ) + : session != null && isOnline + ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) + : ref.watch(gameStorageProvider).count(userId: user?.id); +} + +/// A provider that paginates the game history for a given user, or the current app user if no user is provided. +/// +/// The game history is fetched from the server if the user is logged in and app is online. +/// Otherwise, the game history is fetched from the local storage. +@riverpod +class UserGameHistory extends _$UserGameHistory { + final _list = []; + + @override + Future build( + UserId? userId, { + /// Whether the history is requested in an online context. Applicable only + /// when [userId] is null. + /// + /// If this is true, the provider will attempt to fetch the games from the + /// server. If this is false, the provider will fetch the games from the + /// local storage. + required bool isOnline, + }) async { + ref.cacheFor(const Duration(minutes: 5)); + ref.onDispose(() { + _list.clear(); + }); + + final session = ref.watch(authSessionProvider); + + final recentGames = userId != null + ? ref.read(userRecentGamesProvider(userId: userId).future) + : ref.read(myRecentGamesProvider.future); + + _list.addAll(await recentGames); + + return UserGameHistoryState( + gameList: _list.toIList(), + isLoading: false, + hasMore: true, + hasError: false, + online: isOnline, + session: session, + ); + } + + /// Fetches the next page of games. + void getNext() { + if (!state.hasValue) return; + + final currentVal = state.requireValue; + state = AsyncData(currentVal.copyWith(isLoading: true)); + Result.capture( + userId != null + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + userId!, + max: _nbPerPage, + until: _list.last.game.createdAt, + ), + ) + : currentVal.online && currentVal.session != null + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + currentVal.session!.user.id, + max: _nbPerPage, + until: _list.last.game.createdAt, + ), + ) + : ref + .watch(gameStorageProvider) + .page(max: _nbPerPage, until: _list.last.game.createdAt) + .then( + (value) => value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map( + (e) => ( + game: e.game.data, + pov: e.game.youAre ?? Side.white + ), + ) + .toIList(), + ), + ).fold( + (value) { + if (value.isEmpty) { + state = AsyncData( + currentVal.copyWith(hasMore: false, isLoading: false), + ); + return; + } + + _list.addAll(value); + + state = AsyncData( + currentVal.copyWith( + gameList: _list.toIList(), + isLoading: false, + hasMore: value.length == _nbPerPage, + ), + ); + }, + (error, stackTrace) { + state = + AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + }, + ); + } +} + +@freezed +class UserGameHistoryState with _$UserGameHistoryState { + const factory UserGameHistoryState({ + required IList gameList, + required bool isLoading, + required bool hasMore, + required bool hasError, + required bool online, + AuthSessionState? session, + }) = _UserGameHistoryState; +} diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 510afced48..6834c4851d 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -1,3 +1,4 @@ +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -8,7 +9,7 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; class GameRepository { const GameRepository(this.client); - final http.Client client; + final LichessClient client; Future getGame(GameId id) { return client.readJson( @@ -34,21 +35,39 @@ class GameRepository { } } - Future> getRecentGames(UserId userId) { - return client.readNdJsonList( - Uri( - path: '/api/games/user/$userId', - queryParameters: { - 'max': '20', - 'moves': 'false', - 'lastFen': 'true', - 'accuracy': 'true', - 'opening': 'true', - }, - ), - headers: {'Accept': 'application/x-ndjson'}, - mapper: LightArchivedGame.fromServerJson, - ); + Future> getUserGames( + UserId userId, { + int max = 20, + DateTime? until, + }) { + return client + .readNdJsonList( + Uri( + path: '/api/games/user/$userId', + queryParameters: { + 'max': max.toString(), + if (until != null) + 'until': until.millisecondsSinceEpoch.toString(), + 'moves': 'false', + 'lastFen': 'true', + 'accuracy': 'true', + 'opening': 'true', + }, + ), + headers: {'Accept': 'application/x-ndjson'}, + mapper: LightArchivedGame.fromServerJson, + ) + .then( + (value) => value + .map( + (e) => ( + game: e, + // we know here user is not null for at least one of the players + pov: e.white.user?.id == userId ? Side.white : Side.black, + ), + ) + .toIList(), + ); } /// Returns the games of the current user, given a list of ids. diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index 8650009c17..7d957682d0 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -18,13 +17,6 @@ GameStorage gameStorage( return GameStorage(db); } -@riverpod -Future> recentStoredGames(RecentStoredGamesRef ref) async { - final session = ref.watch(authSessionProvider); - final storage = ref.watch(gameStorageProvider); - return storage.page(userId: session?.user.id); -} - const kGameStorageTable = 'game'; typedef StoredGame = ({ @@ -37,6 +29,17 @@ class GameStorage { const GameStorage(this._db); final Database _db; + Future count({ + UserId? userId, + }) async { + final list = await _db.query( + kGameStorageTable, + where: 'userId = ?', + whereArgs: [userId ?? kStorageAnonId], + ); + return list.length; + } + Future> page({ UserId? userId, DateTime? until, diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 01b3292630..2376b430a6 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; -import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -24,7 +23,7 @@ class CreateGameService { final CreateGameServiceRef ref; final Logger _log; - http.Client get lichessClient => ref.read(lichessClientProvider); + LichessClient get lichessClient => ref.read(lichessClientProvider); StreamSubscription? _pendingGameConnection; diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index 9c582983f9..38cad7d6e2 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -24,7 +24,7 @@ Future> correspondenceChallenges( class LobbyRepository { LobbyRepository(this.client); - final http.Client client; + final LichessClient client; Future createSeek(GameSeek seek, {required String sri}) async { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 25c68dd184..a8cf682f80 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -5,7 +5,6 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; @@ -63,7 +62,8 @@ class PuzzleController extends _$PuzzleController { return _loadNewContext(initialContext, initialStreak); } - PuzzleRepository _repository(http.Client client) => PuzzleRepository(client); + PuzzleRepository _repository(LichessClient client) => + PuzzleRepository(client); PuzzleState _loadNewContext( PuzzleContext context, diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index fceb6dd676..de6028f972 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -26,7 +26,7 @@ part 'puzzle_repository.freezed.dart'; class PuzzleRepository { PuzzleRepository(this.client); - final http.Client client; + final LichessClient client; Future selectBatch({ required int nb, diff --git a/lib/src/model/relation/relation_repository.dart b/lib/src/model/relation/relation_repository.dart index 5efff45e2a..3ecd5bac89 100644 --- a/lib/src/model/relation/relation_repository.dart +++ b/lib/src/model/relation/relation_repository.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; class RelationRepository { const RelationRepository(this.client); - final http.Client client; + final LichessClient client; Future> getFollowing() async { return client.readNdJsonList( diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index c207bd3277..f4c0dcc6ab 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -78,6 +78,7 @@ class User with _$User { required IMap perfs, PlayTime? playTime, Profile? profile, + UserGameCount? count, }) = _User; LightUser get lightUser => LightUser( @@ -106,6 +107,7 @@ class User with _$User { seenAt: pick('seenAt').asDateTimeFromMillisecondsOrNull(), playTime: pick('playTime').letOrNull(PlayTime.fromPick), profile: pick('profile').letOrNull(Profile.fromPick), + count: pick('count').letOrNull(UserGameCount.fromPick), perfs: IMap({ for (final entry in receivedPerfsMap.entries) if (Perf.nameMap.containsKey(entry.key)) @@ -118,6 +120,46 @@ class User with _$User { } } +@freezed +class UserGameCount with _$UserGameCount { + const factory UserGameCount({ + required int all, + // TODO(#454): enable rest of fields when needed for filtering + // required int rated, + // required int ai, + // required int draw, + // required int drawH, + // required int win, + // required int winH, + // required int loss, + // required int lossH, + // required int bookmark, + // required int playing, + // required int imported, + // required int me, + }) = _UserGameCount; + + factory UserGameCount.fromJson(Map json) => + UserGameCount.fromPick(pick(json).required()); + + factory UserGameCount.fromPick(RequiredPick pick) => UserGameCount( + all: pick('all').asIntOrThrow(), + // TODO(#454): enable rest of fields when needed for filtering + // rated: pick('rated').asIntOrThrow(), + // ai: pick('ai').asIntOrThrow(), + // draw: pick('draw').asIntOrThrow(), + // drawH: pick('drawH').asIntOrThrow(), + // win: pick('win').asIntOrThrow(), + // winH: pick('winH').asIntOrThrow(), + // loss: pick('loss').asIntOrThrow(), + // lossH: pick('lossH').asIntOrThrow(), + // bookmark: pick('bookmark').asIntOrThrow(), + // playing: pick('playing').asIntOrThrow(), + // imported: pick('import').asIntOrThrow(), + // me: pick('me').asIntOrThrow(), + ); +} + @freezed class PlayTime with _$PlayTime { const factory PlayTime({ @@ -138,11 +180,14 @@ class PlayTime with _$PlayTime { @freezed class UserPerf with _$UserPerf { + const UserPerf._(); + const factory UserPerf({ required int rating, required int ratingDeviation, required int progression, - required int numberOfGames, + int? games, + int? runs, bool? provisional, }) = _UserPerf; @@ -153,7 +198,8 @@ class UserPerf with _$UserPerf { rating: pick('rating').asIntOrThrow(), ratingDeviation: pick('rd').asIntOrThrow(), progression: pick('prog').asIntOrThrow(), - numberOfGames: pick('games').asIntOrThrow(), + games: pick('games').asIntOrNull(), + runs: pick('runs').asIntOrNull(), provisional: pick('prov').asBoolOrNull(), ); @@ -161,9 +207,11 @@ class UserPerf with _$UserPerf { rating: UserActivityStreak.fromJson(json).score, ratingDeviation: 0, progression: 0, - numberOfGames: UserActivityStreak.fromJson(json).runs, + runs: UserActivityStreak.fromJson(json).runs, provisional: null, ); + + int get numberOfGamesOrRuns => games ?? runs ?? 0; } @freezed diff --git a/lib/src/view/account/profile_screen.dart b/lib/src/view/account/profile_screen.dart index 47f47f85ae..e437bd911c 100644 --- a/lib/src/view/account/profile_screen.dart +++ b/lib/src/view/account/profile_screen.dart @@ -62,7 +62,7 @@ class ProfileScreen extends ConsumerWidget { UserProfile(user: user), const _PerfCards(), const UserActivityWidget(), - const RecentGames(), + const RecentGamesWidget(), ], ); }, @@ -110,7 +110,7 @@ class ProfileScreen extends ConsumerWidget { UserProfile(user: user), const _PerfCards(), const UserActivityWidget(), - const RecentGames(), + const RecentGamesWidget(), ], ), ); diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 89a569fd20..c0b26316a3 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1094,11 +1094,13 @@ class ServerAnalysisSummary extends ConsumerWidget { .read(ctrlProvider.notifier) .requestServerAnalysis() .catchError((Object e) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } }); }); }, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index b5751ef98d..9523436c50 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -4,26 +4,33 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/game/standalone_game_screen.dart'; import 'package:lichess_mobile/src/view/game/status_l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +import 'package:timeago/timeago.dart' as timeago; /// A list tile that shows game info. class GameListTile extends StatelessWidget { const GameListTile({ required this.game, required this.mySide, - required this.oppponentTitle, + required this.opponentTitle, this.icon, this.subtitle, this.trailing, @@ -34,7 +41,7 @@ class GameListTile extends StatelessWidget { final Side mySide; final IconData? icon; - final Widget oppponentTitle; + final Widget opponentTitle; final Widget? subtitle; final Widget? trailing; final GestureTapCallback? onTap; @@ -53,7 +60,7 @@ class GameListTile extends StatelessWidget { builder: (context) => _ContextMenu( game: game, mySide: mySide, - oppponentTitle: oppponentTitle, + oppponentTitle: opponentTitle, icon: icon, subtitle: subtitle, trailing: trailing, @@ -61,7 +68,7 @@ class GameListTile extends StatelessWidget { ); }, leading: icon != null ? Icon(icon) : null, - title: oppponentTitle, + title: opponentTitle, subtitle: subtitle != null ? DefaultTextStyle.merge( child: subtitle!, @@ -211,21 +218,23 @@ class _ContextMenu extends ConsumerWidget { ), BottomSheetContextMenuAction( icon: Icons.biotech, - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.id.value, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), - ), - ); - }, + onPressed: game.variant.isSupported + ? () { + pushPlatformRoute( + context, + builder: (context) => AnalysisScreen( + title: context.l10n.gameAnalysis, + pgnOrId: game.id.value, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: game.variant, + orientation: orientation, + id: game.id, + ), + ), + ); + } + : null, child: Text(context.l10n.gameAnalysis), ), BottomSheetContextMenuAction( @@ -402,3 +411,100 @@ class _ContextMenu extends ConsumerWidget { ); } } + +/// A list tile that shows extended game info including an accuracy meter and a result icon. +class ExtendedGameListTile extends StatelessWidget { + const ExtendedGameListTile({required this.item, this.userId}); + + final LightArchivedGameWithPov item; + final UserId? userId; + + @override + Widget build(BuildContext context) { + final (game: game, pov: youAre) = item; + final me = youAre == Side.white ? game.white : game.black; + final opponent = youAre == Side.white ? game.black : game.white; + + Widget getResultIcon(LightArchivedGame game, Side mySide) { + if (game.status == GameStatus.aborted || + game.status == GameStatus.noStart) { + return const Icon( + CupertinoIcons.xmark_square_fill, + color: LichessColors.grey, + ); + } else { + return game.winner == null + ? Icon( + CupertinoIcons.equal_square_fill, + color: context.lichessColors.brag, + ) + : game.winner == mySide + ? Icon( + CupertinoIcons.plus_square_fill, + color: context.lichessColors.good, + ) + : Icon( + CupertinoIcons.minus_square_fill, + color: context.lichessColors.error, + ); + } + } + + return GameListTile( + game: game, + mySide: youAre, + onTap: game.variant.isSupported + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => game.fullId != null + ? StandaloneGameScreen( + params: InitialStandaloneGameParams( + id: game.fullId!, + ), + ) + : ArchivedGameScreen( + gameData: game, + orientation: youAre, + ), + ); + } + : null, + icon: game.perf.icon, + opponentTitle: UserFullNameWidget.player( + user: opponent.user, + aiLevel: opponent.aiLevel, + rating: opponent.rating, + ), + subtitle: Text( + timeago.format(game.lastMoveAt), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (me.analysis != null) ...[ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.chart_bar_alt_fill, + color: textShade(context, 0.5), + ), + Text( + me.analysis!.accuracy.toString(), + style: TextStyle( + fontSize: 10, + color: textShade(context, Styles.subtitleOpacity), + ), + ), + ], + ), + const SizedBox(width: 5), + ], + getResultIcon(game, youAre), + ], + ), + ); + } +} diff --git a/lib/src/view/game/lobby_screen.dart b/lib/src/view/game/lobby_screen.dart index 1becde68ef..8b71afd518 100644 --- a/lib/src/view/game/lobby_screen.dart +++ b/lib/src/view/game/lobby_screen.dart @@ -1,9 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/navigation.dart'; @@ -68,8 +67,7 @@ class _LobbyScreenState extends ConsumerState with RouteAware { void didPop() { super.didPop(); if (mounted) { - ref.invalidate(accountRecentGamesProvider); - ref.invalidate(recentStoredGamesProvider); + ref.invalidate(myRecentGamesProvider); } } diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 2df92734d3..402c8f9ec2 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/navigation.dart'; @@ -186,8 +186,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { Future _refreshData() { return Future.wait([ - ref.refresh(accountRecentGamesProvider.future), - ref.refresh(recentStoredGamesProvider.future), + ref.refresh(myRecentGamesProvider.future), ref.refresh(ongoingGamesProvider.future), ]); } @@ -220,18 +219,14 @@ class _HomeBody extends ConsumerWidget { data: (status) { final session = ref.watch(authSessionProvider); final isTablet = isTabletOrLarger(context); - final emptyRecent = ref.watch(accountRecentGamesProvider).maybeWhen( - data: (data) => data.isEmpty, - orElse: () => false, - ); - final emptyStored = ref.watch(recentStoredGamesProvider).maybeWhen( + final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( data: (data) => data.isEmpty, orElse: () => false, ); // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) - if (emptyRecent && emptyStored) { + if (emptyRecent) { final welcomeWidgets = [ Padding( padding: Styles.horizontalBodyPadding, @@ -339,7 +334,7 @@ class _HomeBody extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox(height: 8.0), - RecentGames(), + RecentGamesWidget(), ], ), ), @@ -352,7 +347,7 @@ class _HomeBody extends ConsumerWidget { const _OngoingGamesCarousel(maxGamesToShow: 20) else const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), - const RecentGames(), + const RecentGamesWidget(), if (Theme.of(context).platform == TargetPlatform.iOS) const SizedBox(height: 70.0) else diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 578edd5565..9078ecebe1 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -191,17 +191,19 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { case 'redirect': final data = event.data as Map; final gameFullId = pick(data['id']).asGameFullIdOrThrow(); - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return StandaloneGameScreen( - params: InitialStandaloneGameParams( - id: gameFullId, - ), - ); - }, - ); + if (mounted) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return StandaloneGameScreen( + params: InitialStandaloneGameParams( + id: gameFullId, + ), + ); + }, + ); + } widget.setViewMode(_ViewMode.create); case 'reload_seeks': diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index a4099e0b45..53669eacb7 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -564,12 +564,11 @@ class _BottomBar extends ConsumerWidget { makeLabel: (context) => Text( context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value), ), - onPressed: (_) { - ref - .read( + onPressed: (_) async { + final game = await ref.read( archivedGameProvider(id: puzzleState.puzzle.game.id).future, - ) - .then((game) { + ); + if (context.mounted) { pushPlatformRoute( context, builder: (context) => ArchivedGameScreen( @@ -578,7 +577,7 @@ class _BottomBar extends ConsumerWidget { initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, ), ); - }); + } }, ), ], diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 0ff1868a7e..163a6e2bdc 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -130,7 +130,9 @@ class _Body extends ConsumerWidget { ref.listen(ctrlProvider, (prev, state) { if (prev?.mode != StormMode.ended && state.mode == StormMode.ended) { Future.delayed(const Duration(milliseconds: 200), () { - _showStats(context, ref.read(ctrlProvider).stats!); + if (context.mounted) { + _showStats(context, ref.read(ctrlProvider).stats!); + } }); } diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart new file mode 100644 index 0000000000..3555baa437 --- /dev/null +++ b/lib/src/view/user/game_history_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class GameHistoryScreen extends ConsumerWidget { + const GameHistoryScreen({ + required this.user, + required this.isOnline, + super.key, + }); + final LightUser? user; + final bool isOnline; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + ref: ref, + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } + + Widget _buildIos(BuildContext context, WidgetRef ref) { + final nbGamesAsync = ref.watch( + userNumberOfGamesProvider(user, isOnline: isOnline), + ); + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const CupertinoActivityIndicator(), + error: (e, s) => const Text('All Games'), + ), + ), + child: _Body(user: user, isOnline: isOnline), + ); + } + + Widget _buildAndroid(BuildContext context, WidgetRef ref) { + final nbGamesAsync = ref.watch( + userNumberOfGamesProvider(user, isOnline: isOnline), + ); + return Scaffold( + appBar: AppBar( + title: nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const ButtonLoadingIndicator(), + error: (e, s) => const Text('All Games'), + ), + ), + body: _Body(user: user, isOnline: isOnline), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body({required this.user, required this.isOnline}); + + final LightUser? user; + final bool isOnline; + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + final state = ref.read( + userGameHistoryProvider( + widget.user?.id, + isOnline: widget.isOnline, + ), + ); + + if (!state.hasValue) { + return; + } + + final hasMore = state.requireValue.hasMore; + final isLoading = state.requireValue.isLoading; + + if (hasMore && !isLoading) { + ref + .read( + userGameHistoryProvider( + widget.user?.id, + isOnline: widget.isOnline, + ).notifier, + ) + .getNext(); + } + } + } + + @override + Widget build(BuildContext context) { + final gameListState = ref.watch( + userGameHistoryProvider(widget.user?.id, isOnline: widget.isOnline), + ); + + return gameListState.when( + data: (state) { + final list = state.gameList; + + return SafeArea( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', + ), + ), + ); + } + + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + ); + }, + ), + ); + }, + error: (e, s) { + debugPrint( + 'SEVERE: [GameHistoryScreen] could not load game list', + ); + return const Center(child: Text('Could not load Game History')); + }, + loading: () => const CenterLoadingIndicator(), + ); + } +} diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index f3fb288ccf..5f711fc481 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -25,13 +25,13 @@ class PerfCards extends StatelessWidget { List userPerfs = Perf.values.where((element) { final p = user.perfs[element]; return p != null && - p.numberOfGames > 0 && + p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); userPerfs.sort( - (p1, p2) => user.perfs[p1]!.numberOfGames - .compareTo(user.perfs[p2]!.numberOfGames), + (p1, p2) => user.perfs[p1]!.numberOfGamesOrRuns + .compareTo(user.perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 322070ce1f..fa247f316b 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -93,7 +93,8 @@ class _Title extends StatelessWidget { } final p = user.perfs[element]; return p != null && - p.numberOfGames > 0 && + p.games != null && + p.games! > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); return AppBarTextButton( @@ -656,28 +657,27 @@ class _GameListWidget extends ConsumerWidget { children: [ for (final game in games) _GameListTile( - onTap: () { + onTap: () async { final gameIds = ISet(games.map((g) => g.gameId)); - ref - .withClient( + final list = await ref.withClient( (client) => GameRepository(client).getGamesByIds(gameIds), - ) - .then((list) { - final gameData = - list.firstWhereOrNull((g) => g.id == game.gameId); - if (gameData != null && gameData.variant.isSupported) { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => ArchivedGameScreen( - gameData: gameData, - orientation: user.id == gameData.white.user?.id - ? Side.white - : Side.black, - ), - ); - } - }); + ); + final gameData = + list.firstWhereOrNull((g) => g.id == game.gameId); + if (context.mounted && + gameData != null && + gameData.variant.isSupported) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => ArchivedGameScreen( + gameData: gameData, + orientation: user.id == gameData.white.user?.id + ? Side.white + : Side.black, + ), + ); + } }, playerTitle: UserFullNameWidget( user: game.opponent, diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 31184d8564..8339263216 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -1,50 +1,24 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; -import 'package:lichess_mobile/src/view/game/standalone_game_screen.dart'; +import 'package:lichess_mobile/src/view/user/game_history_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:timeago/timeago.dart' as timeago; -part 'recent_games.g.dart'; - -@riverpod -Future> _userRecentGames( - _UserRecentGamesRef ref, { - required UserId userId, -}) { - return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} - -class RecentGames extends ConsumerWidget { - const RecentGames({this.user, super.key}); +/// A widget that show a list of recent games for a given player or the current user. +/// +/// If [user] is not provided, the current logged in user's recent games are displayed. +/// If the current user is not logged in, or there is no connectivity, the stored recent games are displayed instead. +class RecentGamesWidget extends ConsumerWidget { + const RecentGamesWidget({this.user, super.key}); final LightUser? user; @@ -55,62 +29,18 @@ class RecentGames extends ConsumerWidget { final userId = user?.id ?? session?.user.id; final recentGames = user != null - ? ref - .watch(_userRecentGamesProvider(userId: user!.id)) - .whenData((data) { - return data - .map( - (e) => - // user is not null for at least one of the players - (e, e.white.user?.id == userId ? Side.white : Side.black), - ) - .toIList(); - }) - : session != null && - (connectivity.valueOrNull?.isOnline ?? false) == true - ? ref.watch(accountRecentGamesProvider).whenData((data) { - return data - .map( - (e) => ( - e, - // user is not null for at least one of the players - e.white.user?.id == userId ? Side.white : Side.black - ), - ) - .toIList(); - }) - : ref.watch(recentStoredGamesProvider).whenData((data) { - return data - // we can assume that `youAre` is not null either for logged - // in users or for anonymous users - .map((e) => (e.game.data, e.game.youAre ?? Side.white)) - .toIList(); - }); + ? ref.watch(userRecentGamesProvider(userId: user!.id)) + : ref.watch(myRecentGamesProvider); - Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); - } else { - return game.winner == null - ? Icon( - CupertinoIcons.equal_square_fill, - color: context.lichessColors.brag, - ) - : game.winner == mySide - ? Icon( - CupertinoIcons.plus_square_fill, - color: context.lichessColors.good, - ) - : Icon( - CupertinoIcons.minus_square_fill, - color: context.lichessColors.error, - ); - } - } + final nbOfGames = ref + .watch( + userNumberOfGamesProvider( + user, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), + ) + .valueOrNull ?? + 0; return recentGames.when( data: (data) { @@ -120,67 +50,24 @@ class RecentGames extends ConsumerWidget { return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, + headerTrailing: nbOfGames > data.length + ? NoPaddingTextButton( + onPressed: () { + pushPlatformRoute( + context, + builder: (context) => GameHistoryScreen( + user: user, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), + ); + }, + child: Text( + context.l10n.more, + ), + ) + : null, children: data.map((item) { - final (game, youAre) = item; - final me = youAre == Side.white ? game.white : game.black; - final opponent = youAre == Side.white ? game.black : game.white; - - return GameListTile( - game: game, - mySide: youAre, - onTap: game.variant.isSupported - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => game.fullId != null - ? StandaloneGameScreen( - params: InitialStandaloneGameParams( - id: game.fullId!, - ), - ) - : ArchivedGameScreen( - gameData: game, - orientation: youAre, - ), - ); - } - : null, - icon: game.perf.icon, - oppponentTitle: UserFullNameWidget.player( - user: opponent.user, - aiLevel: opponent.aiLevel, - rating: opponent.rating, - ), - subtitle: Text( - timeago.format(game.lastMoveAt), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (me.analysis != null) ...[ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - CupertinoIcons.chart_bar_alt_fill, - color: textShade(context, 0.5), - ), - Text( - me.analysis!.accuracy.toString(), - style: TextStyle( - fontSize: 10, - color: textShade(context, Styles.subtitleOpacity), - ), - ), - ], - ), - const SizedBox(width: 5), - ], - getResultIcon(game, youAre), - ], - ), - ); + return ExtendedGameListTile(item: item, userId: userId); }).toList(), ); }, diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index a5bd77e130..da1bf8d27f 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -119,7 +119,7 @@ class _UserProfileListView extends StatelessWidget { UserProfile(user: user), PerfCards(user: user, isMe: false), UserActivityWidget(user: user), - RecentGames(user: user.lightUser), + RecentGamesWidget(user: user.lightUser), ], ); } diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index f5f231ff29..5733a0348c 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -82,6 +82,10 @@ class CenterLoadingIndicator extends StatelessWidget { } } +/// A screen with an error message and a retry button. +/// +/// This widget is intended to be used when a request fails and the user can +/// retry it. class FullScreenRetryRequest extends StatelessWidget { const FullScreenRetryRequest({ super.key, diff --git a/lib/src/widgets/user_list_tile.dart b/lib/src/widgets/user_list_tile.dart index db9c7ee700..6e6690eb1c 100644 --- a/lib/src/widgets/user_list_tile.dart +++ b/lib/src/widgets/user_list_tile.dart @@ -124,14 +124,16 @@ class _UserRating extends StatelessWidget { List userPerfs = Perf.values.where((element) { final p = perfs[element]; return p != null && - p.numberOfGames > 0 && + p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); if (userPerfs.isEmpty) return const SizedBox.shrink(); userPerfs.sort( - (p1, p2) => perfs[p1]!.numberOfGames.compareTo(perfs[p2]!.numberOfGames), + (p1, p2) => perfs[p1]! + .numberOfGamesOrRuns + .compareTo(perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); diff --git a/test/model/account/account_repository_test.dart b/test/model/account/account_repository_test.dart index fbe1706cd4..1dd7d4f1c8 100644 --- a/test/model/account/account_repository_test.dart +++ b/test/model/account/account_repository_test.dart @@ -2,7 +2,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -59,7 +61,9 @@ void main() { return mockResponse('', 404); }); - final repo = AccountRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = AccountRepository(client); final result = await repo.getPreferences(); expect(result, isA()); diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index 83a878c048..a7603671c0 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -22,7 +22,7 @@ class Listener extends Mock { void call(T? previous, T value); } -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/account') { return mockResponse(testAccountResponse, 200); } else if (request.method == 'DELETE' && request.url.path == '/api/token') { @@ -79,7 +79,8 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -121,7 +122,8 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/model/auth/fake_auth_repository.dart b/test/model/auth/fake_auth_repository.dart index 2fd048b557..c82b5b48da 100644 --- a/test/model/auth/fake_auth_repository.dart +++ b/test/model/auth/fake_auth_repository.dart @@ -32,5 +32,5 @@ const _fakePerf = UserPerf( rating: 1500, ratingDeviation: 0, progression: 0, - numberOfGames: 0, + games: 0, ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index a5b1633c54..1d5311e424 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -2,10 +2,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -24,12 +26,15 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); - final result = await repo.getRecentGames(const UserId('testUser')); - expect(result, isA>()); + final repo = GameRepository(client); + + final result = await repo.getUserGames(const UserId('testUser')); + expect(result, isA>()); expect(result.length, 3); - expect(result[0].id, const GameId('Huk88k3D')); + expect(result[0].game.id, const GameId('Huk88k3D')); }); }); @@ -54,7 +59,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final result = await repo.getGamesByIds(ids); expect(result, isA>()); @@ -76,7 +83,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final game = await repo.getGame(const GameId('qVChCOTc')); expect(game, isA()); @@ -97,7 +106,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final result = await repo.getGame(const GameId('1vdsvmxp')); expect(result, isA()); diff --git a/test/model/game/mock_game_storage.dart b/test/model/game/mock_game_storage.dart index 7107e8002c..0ed719063f 100644 --- a/test/model/game/mock_game_storage.dart +++ b/test/model/game/mock_game_storage.dart @@ -27,4 +27,9 @@ class MockGameStorage implements GameStorage { Future save(ArchivedGame game) { return Future.value(); } + + @override + Future count({UserId? userId}) { + return Future.value(0); + } } diff --git a/test/model/lobby/lobby_repository_test.dart b/test/model/lobby/lobby_repository_test.dart index 1a385cbff4..e790bae327 100644 --- a/test/model/lobby/lobby_repository_test.dart +++ b/test/model/lobby/lobby_repository_test.dart @@ -1,10 +1,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -15,10 +17,12 @@ void main() { return mockResponse('', 404); }); - final repo = LobbyRepository(mockClient); - group('LobbyRepository.getCorrespondenceChallenges', () { test('read json', () async { + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = LobbyRepository(client); + final data = await repo.getCorrespondenceChallenges(); expect(data, isA>()); diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 7188db22f9..0f884807d0 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -9,16 +8,6 @@ import '../../test_container.dart'; import '../../test_utils.dart'; void main() { - Future makeTestContainer(MockClient mockClient) async { - return makeContainer( - overrides: [ - lichessClientProvider.overrideWith((ref) { - return mockClient; - }), - ], - ); - } - group('PuzzleRepository', () { test('selectBatch', () async { final mockClient = MockClient((request) { @@ -33,7 +22,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -56,7 +45,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -79,7 +68,7 @@ void main() { } return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -103,7 +92,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); final result = await repo.streak(); @@ -124,7 +113,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); final result = await repo.puzzleDashboard(30); diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index b7f8a78b68..734b3b15b2 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -33,7 +33,7 @@ void main() { return db; }), lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), ], ); diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index f90ced3eff..5800d76431 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -1,9 +1,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -23,7 +25,9 @@ void main() { return mockResponse('', 404); }); - final repo = RelationRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = RelationRepository(client); final result = await repo.getFollowing(); expect(result, isA>()); @@ -43,7 +47,9 @@ void main() { return mockResponse('', 404); }); - final repo = RelationRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = RelationRepository(client); final result = await repo.getFollowing(); expect(result, isA>()); diff --git a/test/test_app.dart b/test/test_app.dart index 95deb16706..f88e881b9a 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -83,9 +83,13 @@ Future buildTestApp( return ProviderScope( overrides: [ // ignore: scoped_providers_should_specify_dependencies - lichessClientProvider.overrideWithValue(mockClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), // ignore: scoped_providers_should_specify_dependencies - defaultClientProvider.overrideWithValue(mockClient), + defaultClientProvider.overrideWith((_) { + return mockClient; + }), // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); diff --git a/test/test_container.dart b/test/test_container.dart index 5a363bf20f..042f230b2f 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/app_dependencies.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; @@ -38,6 +39,19 @@ class MockHttpClient extends Mock implements http.Client {} const shouldLog = false; +/// Returns a [ProviderContainer] with a mocked [LichessClient] configured with +/// the given [mockClient]. +Future lichessClientContainer(MockClient mockClient) async { + return makeContainer( + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); +} + +/// Returns a [ProviderContainer] with default mocks, ready for testing. Future makeContainer({ List? overrides, AuthSessionState? userSession, @@ -68,7 +82,9 @@ Future makeContainer({ ref.onDispose(pool.dispose); return pool; }), - lichessClientProvider.overrideWithValue(MockHttpClient()), + lichessClientProvider.overrideWith((ref) { + return LichessClient(MockHttpClient(), ref); + }), connectivityProvider.overrideWith((ref) { return Stream.value( const ConnectivityStatus( diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index c8a22cfdef..4c699279b3 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -20,7 +20,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/game/export/qVChCOTc') { return mockResponse(gameResponse, 200); } @@ -39,7 +39,8 @@ void main() { orientation: Side.white, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -111,7 +112,9 @@ void main() { orientation: Side.white, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index e0b91529e7..04f6e6e671 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -156,7 +156,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; @@ -270,7 +270,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; @@ -370,7 +370,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 9acd6787ec..7e38602448 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/view/puzzle/storm_screen.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/storm') { return mockResponse('', 200); } @@ -32,7 +32,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -52,7 +53,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -75,7 +77,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -129,7 +132,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -173,7 +177,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/settings/settings_screen_test.dart b/test/view/settings/settings_screen_test.dart index 5ae01e6eec..4f63a24d4b 100644 --- a/test/view/settings/settings_screen_test.dart +++ b/test/view/settings/settings_screen_test.dart @@ -11,7 +11,7 @@ import '../../model/auth/fake_session_storage.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.method == 'DELETE' && request.url.path == '/api/token') { return mockResponse('ok', 200); } @@ -65,7 +65,8 @@ void main() { home: const SettingsScreen(), userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index 749be400ff..f060f3cf28 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player') { return mockResponse(leaderboardResponse, 200); } @@ -24,7 +24,8 @@ void main() { final app = await buildTestApp( tester, overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], home: const LeaderboardScreen(), ); diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index 0e0b55a135..b51d501dda 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player/top/1/standard') { return mockResponse(top1Response, 200); } @@ -25,7 +25,8 @@ void main() { tester, home: Column(children: [LeaderboardWidget()]), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index 7d363dfa32..a5791e4ca8 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -10,7 +10,7 @@ import '../../model/auth/fake_auth_repository.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/user/${fakeUser.id}/perf/${testPerf.name}') { return mockResponse(userPerfStatsResponse, 200); } @@ -34,7 +34,9 @@ void main() { perf: testPerf, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); @@ -64,7 +66,9 @@ void main() { perf: testPerf, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index f6e2da6d1f..9691567d41 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player/autocomplete') { if (request.url.queryParameters['term'] == 'joh') { return mockResponse(johResponse, 200); @@ -29,7 +29,8 @@ void main() { tester, home: const SearchScreen(), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -69,7 +70,8 @@ void main() { tester, home: const SearchScreen(), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index 51e404c6af..f3e52f2830 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -9,7 +9,7 @@ import '../../model/user/user_repository_test.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/games/user/$testUserId') { return mockResponse(userGameResponse, 200); } else if (request.url.path == '/api/user/$testUserId') { @@ -42,7 +42,8 @@ void main() { tester, home: const UserScreen(user: testUser), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], );