Skip to content

Commit 0a6508e

Browse files
committed
Merge branch 'MichaelSpece-codex/signed-out-account-preferences'
2 parents 8b188b9 + 6b585d4 commit 0a6508e

9 files changed

Lines changed: 957 additions & 513 deletions

File tree

lib/src/model/account/account_preferences.dart

Lines changed: 186 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,58 @@
1+
import 'dart:convert';
2+
13
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
24
import 'package:flutter/widgets.dart';
35
import 'package:flutter_riverpod/flutter_riverpod.dart';
6+
import 'package:freezed_annotation/freezed_annotation.dart';
47
import 'package:lichess_mobile/l10n/l10n.dart';
8+
import 'package:lichess_mobile/src/binding.dart';
59
import 'package:lichess_mobile/src/model/account/account_repository.dart';
610
import 'package:lichess_mobile/src/model/auth/auth_controller.dart';
11+
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
712

8-
typedef AccountPrefState = ({
9-
// game display
10-
Zen zenMode,
11-
PieceNotation pieceNotation,
12-
ShowRatings showRatings,
13-
// game behavior
14-
BooleanPref premove,
15-
AutoQueen autoQueen,
16-
AutoThreefold autoThreefold,
17-
Takeback takeback,
18-
BooleanPref confirmResign,
19-
SubmitMove submitMove,
20-
// clock
21-
Moretime moretime,
22-
ClockTenths clockTenths,
23-
BooleanPref clockSound,
24-
// privacy
25-
BooleanPref follow,
26-
Challenge challenge,
27-
Message message,
28-
});
29-
30-
/// A provider that tells if the user wants to see ratings in the app.
31-
final showRatingsPrefProvider = FutureProvider<ShowRatings>((Ref ref) async {
32-
final prefs = await ref.watch(accountPreferencesProvider.future);
33-
return prefs?.showRatings ?? defaultAccountPreferences.showRatings;
34-
});
13+
part 'account_preferences.freezed.dart';
14+
part 'account_preferences.g.dart';
3515

36-
/// A provider that tells if the user wants clock sounds.
37-
final clockSoundProvider = FutureProvider<bool>((Ref ref) async {
38-
final prefs = await ref.watch(accountPreferencesProvider.future);
39-
return prefs?.clockSound.value ?? defaultAccountPreferences.clockSound.value;
40-
});
41-
42-
/// A provider that gives the user's preferred piece notation.
43-
final pieceNotationProvider = FutureProvider<PieceNotation>((Ref ref) async {
44-
final prefs = await ref.watch(accountPreferencesProvider.future);
45-
return prefs?.pieceNotation ?? defaultAccountPreferences.pieceNotation;
46-
});
16+
/// Represents the account preferences for a user.
17+
///
18+
/// If the user is authenticated, the preferences are fetched from the server. If the user is not authenticated, the preferences are fetched from local storage.
19+
/// Technically an anonymous user does not have account preferences, but we still store them locally for convenience, because some settings are useful even for anonymous users (e.g. piece notation, premoves, etc.).
20+
@Freezed(fromJson: true, toJson: true)
21+
sealed class AccountPrefState with _$AccountPrefState implements Serializable {
22+
const AccountPrefState._();
23+
24+
const factory AccountPrefState({
25+
// game display
26+
@JsonKey(unknownEnumValue: Zen.no) required Zen zenMode,
27+
@JsonKey(unknownEnumValue: PieceNotation.symbol) required PieceNotation pieceNotation,
28+
@JsonKey(unknownEnumValue: ShowRatings.yes) required ShowRatings showRatings,
29+
// game behavior
30+
required BooleanPref premove,
31+
@JsonKey(unknownEnumValue: AutoQueen.premove) required AutoQueen autoQueen,
32+
@JsonKey(unknownEnumValue: AutoThreefold.always) required AutoThreefold autoThreefold,
33+
@JsonKey(unknownEnumValue: Takeback.always) required Takeback takeback,
34+
required BooleanPref confirmResign,
35+
required SubmitMove submitMove,
36+
// clock
37+
@JsonKey(unknownEnumValue: Moretime.always) required Moretime moretime,
38+
@JsonKey(unknownEnumValue: ClockTenths.lessThan10s) required ClockTenths clockTenths,
39+
required BooleanPref clockSound,
40+
// privacy
41+
required BooleanPref follow,
42+
@JsonKey(unknownEnumValue: Challenge.registered) required Challenge challenge,
43+
@JsonKey(unknownEnumValue: Message.always) required Message message,
44+
}) = _AccountPrefState;
45+
46+
factory AccountPrefState.fromJson(Map<String, dynamic> json) {
47+
try {
48+
return _$AccountPrefStateFromJson(json);
49+
} catch (_) {
50+
return defaultAccountPreferences;
51+
}
52+
}
53+
}
4754

48-
final defaultAccountPreferences = (
55+
final defaultAccountPreferences = AccountPrefState(
4956
zenMode: Zen.no,
5057
pieceNotation: PieceNotation.symbol,
5158
showRatings: ShowRatings.yes,
@@ -63,8 +70,32 @@ final defaultAccountPreferences = (
6370
message: Message.always,
6471
);
6572

73+
/// A provider that tells if the user wants to see ratings in the app.
74+
final showRatingsPrefProvider = FutureProvider<ShowRatings>((Ref ref) async {
75+
final prefs = await ref.watch(accountPreferencesProvider.future);
76+
return prefs.showRatings;
77+
});
78+
79+
/// A provider that tells if the user wants clock sounds.
80+
final clockSoundProvider = FutureProvider<bool>((Ref ref) async {
81+
final prefs = await ref.watch(accountPreferencesProvider.future);
82+
return prefs.clockSound.value;
83+
});
84+
85+
/// A provider that gives the user's preferred piece notation.
86+
final pieceNotationProvider = FutureProvider<PieceNotation>((Ref ref) async {
87+
final prefs = await ref.watch(accountPreferencesProvider.future);
88+
return prefs.pieceNotation;
89+
});
90+
91+
/// A provider that gives the user's preferred clock tenths display.
92+
final clockTenthsProvider = FutureProvider<ClockTenths>((Ref ref) async {
93+
final prefs = await ref.watch(accountPreferencesProvider.future);
94+
return prefs.clockTenths;
95+
});
96+
6697
/// A provider that gives the account preferences for the current user.
67-
final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, AccountPrefState?>(
98+
final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, AccountPrefState>(
6899
AccountPreferences.new,
69100
name: 'AccountPreferencesProvider',
70101
);
@@ -73,45 +104,99 @@ final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, Acc
73104
///
74105
/// The result is cached for the lifetime of the app, until refreshed.
75106
/// If the server returns an error, default values are returned.
76-
class AccountPreferences extends AsyncNotifier<AccountPrefState?> {
107+
///
108+
/// If the user is authenticated, the preferences are fetched from the server. If the user is not authenticated, the preferences are fetched from local storage.
109+
/// See also [AccountPrefState] for more details.
110+
class AccountPreferences extends AsyncNotifier<AccountPrefState> {
77111
@override
78-
Future<AccountPrefState?> build() async {
112+
Future<AccountPrefState> build() async {
79113
final authUser = ref.watch(authControllerProvider);
80114

81115
if (authUser == null) {
82-
return null;
116+
return _fetchLocal();
83117
}
84118

85119
try {
86-
return ref.read(accountRepositoryProvider).getPreferences();
120+
return await ref.read(accountRepositoryProvider).getPreferences();
87121
} catch (e) {
88122
debugPrint('[AccountPreferences] Error getting account preferences: $e');
89123
return defaultAccountPreferences;
90124
}
91125
}
92126

93-
Future<void> setZen(Zen value) => _setPref('zen', value);
94-
Future<void> setPieceNotation(PieceNotation value) => _setPref('pieceNotation', value);
95-
Future<void> setShowRatings(ShowRatings value) => _setPref('ratings', value);
96-
97-
Future<void> setPremove(BooleanPref value) => _setPref('premove', value);
98-
Future<void> setTakeback(Takeback value) => _setPref('takeback', value);
99-
Future<void> setAutoQueen(AutoQueen value) => _setPref('autoQueen', value);
100-
Future<void> setAutoThreefold(AutoThreefold value) => _setPref('autoThreefold', value);
101-
Future<void> setMoretime(Moretime value) => _setPref('moretime', value);
102-
Future<void> setClockTenths(ClockTenths value) => _setPref('clockTenths', value);
103-
Future<void> setClockSound(BooleanPref value) => _setPref('clockSound', value);
104-
Future<void> setConfirmResign(BooleanPref value) => _setPref('confirmResign', value);
105-
Future<void> setSubmitMove(SubmitMove value) => _setPref('submitMove', value);
106-
Future<void> setFollow(BooleanPref value) => _setPref('follow', value);
107-
Future<void> setChallenge(Challenge value) => _setPref('challenge', value);
108-
Future<void> setMessage(Message value) => _setPref('message', value);
109-
110-
Future<void> _setPref<T>(String key, AccountPref<T> value) async {
127+
Future<void> setZen(Zen value) =>
128+
_setPref('zen', value, (prefs) => prefs.copyWith(zenMode: value));
129+
Future<void> setPieceNotation(PieceNotation value) =>
130+
_setPref('pieceNotation', value, (prefs) => prefs.copyWith(pieceNotation: value));
131+
Future<void> setShowRatings(ShowRatings value) =>
132+
_setPref('ratings', value, (prefs) => prefs.copyWith(showRatings: value));
133+
134+
Future<void> setPremove(BooleanPref value) =>
135+
_setPref('premove', value, (prefs) => prefs.copyWith(premove: value));
136+
Future<void> setTakeback(Takeback value) =>
137+
_setPref('takeback', value, (prefs) => prefs.copyWith(takeback: value));
138+
Future<void> setAutoQueen(AutoQueen value) =>
139+
_setPref('autoQueen', value, (prefs) => prefs.copyWith(autoQueen: value));
140+
Future<void> setAutoThreefold(AutoThreefold value) =>
141+
_setPref('autoThreefold', value, (prefs) => prefs.copyWith(autoThreefold: value));
142+
Future<void> setMoretime(Moretime value) =>
143+
_setPref('moretime', value, (prefs) => prefs.copyWith(moretime: value));
144+
Future<void> setClockTenths(ClockTenths value) =>
145+
_setPref('clockTenths', value, (prefs) => prefs.copyWith(clockTenths: value));
146+
Future<void> setClockSound(BooleanPref value) =>
147+
_setPref('clockSound', value, (prefs) => prefs.copyWith(clockSound: value));
148+
Future<void> setConfirmResign(BooleanPref value) =>
149+
_setPref('confirmResign', value, (prefs) => prefs.copyWith(confirmResign: value));
150+
Future<void> setSubmitMove(SubmitMove value) =>
151+
_setPref('submitMove', value, (prefs) => prefs.copyWith(submitMove: value));
152+
Future<void> setFollow(BooleanPref value) =>
153+
_setPref('follow', value, (prefs) => prefs.copyWith(follow: value));
154+
Future<void> setChallenge(Challenge value) =>
155+
_setPref('challenge', value, (prefs) => prefs.copyWith(challenge: value));
156+
Future<void> setMessage(Message value) =>
157+
_setPref('message', value, (prefs) => prefs.copyWith(message: value));
158+
159+
Future<void> _setPref<T>(
160+
String key,
161+
AccountPref<T> value,
162+
AccountPrefState Function(AccountPrefState prefs) updateLocal,
163+
) async {
164+
final authUser = ref.read(authControllerProvider);
165+
if (authUser == null) {
166+
await _saveLocal(updateLocal(state.value ?? _fetchLocal()));
167+
return;
168+
}
169+
111170
await Future<void>.delayed(const Duration(milliseconds: 200));
112171
await ref.read(accountRepositoryProvider).setPreference(key, value);
113172
ref.invalidateSelf();
114173
}
174+
175+
Future<void> _saveLocal(AccountPrefState value) async {
176+
await LichessBinding.instance.sharedPreferences.setString(
177+
PrefCategory.account.storageKey,
178+
jsonEncode(value.toJson()),
179+
);
180+
181+
if (!ref.mounted) return;
182+
183+
state = AsyncValue.data(value);
184+
}
185+
186+
AccountPrefState _fetchLocal() {
187+
final stored = LichessBinding.instance.sharedPreferences.getString(
188+
PrefCategory.account.storageKey,
189+
);
190+
if (stored == null) {
191+
return defaultAccountPreferences;
192+
}
193+
try {
194+
return AccountPrefState.fromJson(jsonDecode(stored) as Map<String, dynamic>);
195+
} catch (e) {
196+
debugPrint('[AccountPreferences] Error reading local account preferences: $e');
197+
return defaultAccountPreferences;
198+
}
199+
}
115200
}
116201

117202
abstract class AccountPref<T> {
@@ -128,6 +213,10 @@ class BooleanPref implements AccountPref<bool> {
128213
@override
129214
String get toFormData => value ? '1' : '0';
130215

216+
factory BooleanPref.fromJson(bool json) => BooleanPref(json);
217+
218+
bool toJson() => value;
219+
131220
static BooleanPref fromInt(int value) {
132221
switch (value) {
133222
case 1:
@@ -141,8 +230,11 @@ class BooleanPref implements AccountPref<bool> {
141230
}
142231

143232
enum Zen implements AccountPref<int> {
233+
@JsonValue(0)
144234
no(0),
235+
@JsonValue(1)
145236
yes(1),
237+
@JsonValue(2)
146238
gameAuto(2);
147239

148240
const Zen(this.value);
@@ -179,8 +271,11 @@ enum Zen implements AccountPref<int> {
179271
}
180272

181273
enum ShowRatings implements AccountPref<int> {
274+
@JsonValue(0)
182275
no(0),
276+
@JsonValue(1)
183277
yes(1),
278+
@JsonValue(2)
184279
exceptInGame(2);
185280

186281
const ShowRatings(this.value);
@@ -217,7 +312,9 @@ enum ShowRatings implements AccountPref<int> {
217312
}
218313

219314
enum PieceNotation implements AccountPref<int> {
315+
@JsonValue(0)
220316
symbol(0),
317+
@JsonValue(1)
221318
letter(1);
222319

223320
const PieceNotation(this.value);
@@ -250,8 +347,11 @@ enum PieceNotation implements AccountPref<int> {
250347
}
251348

252349
enum AutoQueen implements AccountPref<int> {
350+
@JsonValue(1)
253351
never(1),
352+
@JsonValue(2)
254353
premove(2),
354+
@JsonValue(3)
255355
always(3);
256356

257357
const AutoQueen(this.value);
@@ -288,8 +388,11 @@ enum AutoQueen implements AccountPref<int> {
288388
}
289389

290390
enum AutoThreefold implements AccountPref<int> {
391+
@JsonValue(1)
291392
never(1),
393+
@JsonValue(2)
292394
time(2),
395+
@JsonValue(3)
293396
always(3);
294397

295398
const AutoThreefold(this.value);
@@ -326,8 +429,11 @@ enum AutoThreefold implements AccountPref<int> {
326429
}
327430

328431
enum Takeback implements AccountPref<int> {
432+
@JsonValue(1)
329433
never(1),
434+
@JsonValue(2)
330435
casual(2),
436+
@JsonValue(3)
331437
always(3);
332438

333439
const Takeback(this.value);
@@ -364,8 +470,11 @@ enum Takeback implements AccountPref<int> {
364470
}
365471

366472
enum Moretime implements AccountPref<int> {
473+
@JsonValue(1)
367474
never(1),
475+
@JsonValue(2)
368476
casual(2),
477+
@JsonValue(3)
369478
always(3);
370479

371480
const Moretime(this.value);
@@ -402,8 +511,11 @@ enum Moretime implements AccountPref<int> {
402511
}
403512

404513
enum ClockTenths implements AccountPref<int> {
514+
@JsonValue(0)
405515
never(0),
516+
@JsonValue(1)
406517
lessThan10s(1),
518+
@JsonValue(2)
407519
always(2);
408520

409521
const ClockTenths(this.value);
@@ -440,10 +552,15 @@ enum ClockTenths implements AccountPref<int> {
440552
}
441553

442554
enum Challenge implements AccountPref<int> {
555+
@JsonValue(1)
443556
never(1),
557+
@JsonValue(2)
444558
rating(2),
559+
@JsonValue(3)
445560
friends(3),
561+
@JsonValue(4)
446562
registered(4),
563+
@JsonValue(5)
447564
always(5);
448565

449566
const Challenge(this.value);
@@ -488,8 +605,11 @@ enum Challenge implements AccountPref<int> {
488605
}
489606

490607
enum Message implements AccountPref<int> {
608+
@JsonValue(1)
491609
never(1),
610+
@JsonValue(2)
492611
friends(2),
612+
@JsonValue(3)
493613
always(3);
494614

495615
const Message(this.value);
@@ -546,6 +666,10 @@ class SubmitMove implements AccountPref<int> {
546666

547667
factory SubmitMove.fromInt(int value) =>
548668
SubmitMove(SubmitMoveChoice.values.where((choice) => _bitPresent(value, choice.value)));
669+
670+
factory SubmitMove.fromJson(int json) => SubmitMove.fromInt(json);
671+
672+
int toJson() => value;
549673
}
550674

551675
enum SubmitMoveChoice {

0 commit comments

Comments
 (0)