Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lib/src/model/user/search_history.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:convert';

import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/db/shared_preferences.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'search_history.g.dart';
part 'search_history.freezed.dart';

@riverpod
class SearchHistory extends _$SearchHistory {
static const prefKey = 'search.history';
static const maxHistory = 10;

@override
SearchHistoryState build() {
final prefs = ref.watch(sharedPreferencesProvider);
final stored = prefs.getString(prefKey);

return stored != null
? SearchHistoryState.fromJson(
jsonDecode(stored) as Map<String, dynamic>,
)
: SearchHistoryState(history: IList());
}

Future<void> saveTerm(String term) async {
if (state.history.contains(term)) {
return;
}
final currentList = state.history.toList();
if (currentList.length >= maxHistory) {
currentList.removeLast();
}
currentList.insert(0, term);
final newState = SearchHistoryState(history: currentList.toIList());
final prefs = ref.read(sharedPreferencesProvider);
await prefs.setString(prefKey, jsonEncode(newState.toJson()));
state = newState;
}

Future<void> clear() async {
final newState = state.copyWith(history: IList());
final prefs = ref.read(sharedPreferencesProvider);
await prefs.setString(prefKey, jsonEncode(newState.toJson()));
state = newState;
}
}

@Freezed(fromJson: true, toJson: true)
class SearchHistoryState with _$SearchHistoryState {
const factory SearchHistoryState({
required IList<String> history,
}) = _SearchHistoryState;

factory SearchHistoryState.fromJson(Map<String, dynamic> json) =>
_$SearchHistoryStateFromJson(json);
}
2 changes: 2 additions & 0 deletions lib/src/model/user/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class LightUser with _$LightUser {
String? title,
String? flair,
bool? isPatron,
bool? isOnline,
}) = _LightUser;

factory LightUser.fromJson(Map<String, dynamic> json) =>
Expand All @@ -41,6 +42,7 @@ extension LightUserExtension on Pick {
title: requiredPick('title').asStringOrNull(),
flair: requiredPick('flair').asStringOrNull(),
isPatron: requiredPick('patron').asBoolOrNull(),
isOnline: requiredPick('online').asBoolOrNull(),
);
}
throw PickException(
Expand Down
24 changes: 24 additions & 0 deletions lib/src/model/user/user_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,33 @@ class UserRepository {
);
});
}

FutureResult<IList<LightUser>> autocompleteUser(String term) {
return apiClient
.get(
Uri.parse(
'$kLichessHost/api/player/autocomplete?term=$term&friend=1&object=1',
),
)
.flatMap(
(response) => readJsonObjectFromResponse(
response,
mapper: _autocompleteFromJson,
logger: _log,
),
);
}
}

// --
IList<LightUser> _autocompleteFromJson(Map<String, dynamic> json) =>
_autocompleteFromPick(pick(json).required());

IList<LightUser> _autocompleteFromPick(RequiredPick pick) {
return pick('result')
.asListOrThrow((userPick) => userPick.asLightUserOrThrow())
.toIList();
}

UserActivity _userActivityFromJson(Map<String, dynamic> json) =>
_userActivityFromPick(pick(json).required());
Expand Down
20 changes: 20 additions & 0 deletions lib/src/model/user/user_repository_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'user_repository.dart';

part 'user_repository_providers.g.dart';

const _kAutoCompleteDebounceTimer = Duration(milliseconds: 300);

@Riverpod(keepAlive: true)
UserRepository userRepository(UserRepositoryRef ref) {
final apiClient = ref.watch(authClientProvider);
Expand Down Expand Up @@ -101,3 +103,21 @@ Future<Leaderboard> leaderboard(LeaderboardRef ref) async {
}
return result.asFuture;
}

@riverpod
Future<IList<LightUser>> autoCompleteUser(
AutoCompleteUserRef ref,
String term,
) async {
// debounce calls as user might be typing
var didDispose = false;
ref.onDispose(() => didDispose = true);
await Future<void>.delayed(_kAutoCompleteDebounceTimer);
if (didDispose) {
throw Exception('Cancelled');
}

final repo = ref.watch(userRepositoryProvider);
final result = await repo.autocompleteUser(term);
return result.asFuture;
}
26 changes: 24 additions & 2 deletions lib/src/view/home/home_tab_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:lichess_mobile/src/view/account/profile_screen.dart';
import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart';
import 'package:lichess_mobile/src/view/auth/sign_in_widget.dart';
import 'package:lichess_mobile/src/view/game/lobby_screen.dart';
import 'package:lichess_mobile/src/view/home/search_screen.dart';
import 'package:lichess_mobile/src/view/play/offline_correspondence_games_screen.dart';
import 'package:lichess_mobile/src/view/play/ongoing_games_screen.dart';
import 'package:lichess_mobile/src/view/play/play_screen.dart';
Expand Down Expand Up @@ -117,11 +118,12 @@ class _HomeScreenState extends ConsumerState<HomeTabScreen> with RouteAware {
},
),
actions: [
const _SearchButton(),
const _SettingsButton(),
if (session != null)
const _RelationButton()
else
const SignInWidget(),
const _SettingsButton(),
],
),
body: RefreshIndicator(
Expand Down Expand Up @@ -160,8 +162,9 @@ class _HomeScreenState extends ConsumerState<HomeTabScreen> with RouteAware {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (session != null) const _RelationButton(),
const _SearchButton(),
const _SettingsButton(),
if (session != null) const _RelationButton(),
],
),
),
Expand Down Expand Up @@ -430,6 +433,25 @@ class _HomeBody extends ConsumerWidget {
}
}

class _SearchButton extends StatelessWidget {
const _SearchButton();

@override
Widget build(BuildContext context) {
return AppBarIconButton(
icon: const Icon(Icons.search),
semanticsLabel: 'Search Lichess',
onPressed: () {
pushPlatformRoute(
context,
fullscreenDialog: true,
builder: (_) => const SearchScreen(),
);
},
);
}
}

class _SettingsButton extends StatelessWidget {
const _SettingsButton();

Expand Down
Loading