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
3 changes: 3 additions & 0 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/app_links.dart';
import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/model/account/account_repository.dart';
import 'package:lichess_mobile/src/model/account/account_service.dart';
import 'package:lichess_mobile/src/model/challenge/challenge_service.dart';
import 'package:lichess_mobile/src/model/common/preloaded_data.dart';
import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart';
Expand Down Expand Up @@ -78,6 +79,8 @@ class _AppState extends ConsumerState<Application> {
// Start services
ref.read(notificationServiceProvider).start();
ref.read(challengeServiceProvider).start();
ref.read(accountServiceProvider).start();
ref.read(correspondenceServiceProvider).start();

// Listen for connectivity changes and perform actions accordingly.
ref.listenManual(connectivityChangesProvider, (prev, current) async {
Expand Down
25 changes: 4 additions & 21 deletions lib/src/model/account/account_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,17 @@ Future<IList<OngoingGame>> ongoingGames(Ref ref) async {
);
}

@Riverpod(keepAlive: true)
AccountService accountService(Ref ref) {
return AccountService(ref);
}

class AccountService {
const AccountService(this._ref);

final Ref _ref;

Future<void> setGameBookmark(GameId id, {required bool bookmark}) async {
final session = _ref.read(authSessionProvider);
if (session == null) return;

await _ref.withClient((client) => AccountRepository(client).bookmark(id, bookmark: bookmark));

_ref.invalidate(accountProvider);
}
}

class AccountRepository {
AccountRepository(this.client);

final LichessClient client;
final Logger _log = Logger('AccountRepository');

Future<User> getProfile() {
return client.readJson(Uri(path: '/api/account'), mapper: User.fromServerJson);
return client.readJson(
Uri(path: '/api/account', queryParameters: {'playban': '1'}),
mapper: User.fromServerJson,
);
}

Future<void> saveProfile(Map<String, String> profile) async {
Expand Down
115 changes: 115 additions & 0 deletions lib/src/model/account/account_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'dart:async';

import 'package:flutter/material.dart' show Navigator, Text, showAdaptiveDialog;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/binding.dart' show LichessBinding;
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/id.dart';
import 'package:lichess_mobile/src/model/notifications/notification_service.dart';
import 'package:lichess_mobile/src/model/notifications/notifications.dart'
show LocalNotification, PlaybanNotification;
import 'package:lichess_mobile/src/model/user/user.dart' show TemporaryBan, User;
import 'package:lichess_mobile/src/navigation.dart' show currentNavigatorKeyProvider;
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/view/play/playban.dart';
import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'account_service.g.dart';

@Riverpod(keepAlive: true)
AccountService accountService(Ref ref) {
final service = AccountService(ref);
ref.onDispose(() {
service.dispose();
});
return service;
}

class AccountService {
AccountService(this._ref);

ProviderSubscription<AsyncValue<User?>>? _accountProviderSubscription;
StreamSubscription<(NotificationResponse, LocalNotification)>? _notificationResponseSubscription;

final Ref _ref;

static const _storageKey = 'account.playban_notification_date';

void start() {
final prefs = LichessBinding.instance.sharedPreferences;

_accountProviderSubscription = _ref.listen(accountProvider, (_, account) {
final playban = account.valueOrNull?.playban;
final storedDate = prefs.getString(_storageKey);
final lastPlaybanNotificationDate = storedDate != null ? DateTime.parse(storedDate) : null;

if (playban != null && lastPlaybanNotificationDate != playban.date) {
_savePlaybanNotificationDate(playban.date);
_ref.read(notificationServiceProvider).show(PlaybanNotification(playban));
} else if (playban == null && lastPlaybanNotificationDate != null) {
_ref
.read(notificationServiceProvider)
.cancel(lastPlaybanNotificationDate.toIso8601String().hashCode);
_clearPlaybanNotificationDate();
}
});

_notificationResponseSubscription = NotificationService.responseStream.listen((data) {
final (_, notification) = data;
switch (notification) {
case PlaybanNotification(:final playban):
_onPlaybanNotificationResponse(playban);
case _:
break;
}
});
}

void _savePlaybanNotificationDate(DateTime date) {
LichessBinding.instance.sharedPreferences.setString(_storageKey, date.toIso8601String());
}

void _clearPlaybanNotificationDate() {
LichessBinding.instance.sharedPreferences.remove(_storageKey);
}

void dispose() {
_accountProviderSubscription?.close();
_notificationResponseSubscription?.cancel();
}

Future<void> _onPlaybanNotificationResponse(TemporaryBan playban) async {
final context = _ref.read(currentNavigatorKeyProvider).currentContext;
if (context == null || !context.mounted) return;

return showAdaptiveDialog(
context: context,
barrierDismissible: true,
builder: (context) {
return PlatformAlertDialog(
content: PlaybanMessage(playban: playban, centerText: true),
actions: [
PlatformDialogAction(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

Future<void> setGameBookmark(GameId id, {required bool bookmark}) async {
final session = _ref.read(authSessionProvider);
if (session == null) return;

await _ref.withClient((client) => AccountRepository(client).bookmark(id, bookmark: bookmark));

_ref.invalidate(accountProvider);
}
}
17 changes: 15 additions & 2 deletions lib/src/model/challenge/challenge_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:deep_pick/deep_pick.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/challenge/challenge.dart';
import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart';
Expand Down Expand Up @@ -38,6 +39,7 @@ class ChallengeService {
ChallengesList? _previous;

StreamSubscription<ChallengesList>? _socketSubscription;
StreamSubscription<(NotificationResponse, LocalNotification)>? _notificationResponseSubscription;

/// The stream of challenge events that are received from the server.
static Stream<ChallengesList> get stream =>
Expand All @@ -49,9 +51,19 @@ class ChallengeService {
return (inward: inward.lock, outward: outward.lock);
}).whereNotNull();

/// Start listening to challenge events from the server.
/// Start listening to events.
void start() {
_socketSubscription = stream.listen(_onSocketEvent);

_notificationResponseSubscription = NotificationService.responseStream.listen((data) {
final (response, notification) = data;
switch (notification) {
case ChallengeNotification(:final challenge):
_onNotificationResponse(response.actionId, challenge);
case _:
break;
}
});
}

void _onSocketEvent(ChallengesList current) {
Expand Down Expand Up @@ -90,10 +102,11 @@ class ChallengeService {
/// Stop listening to challenge events from the server.
void dispose() {
_socketSubscription?.cancel();
_notificationResponseSubscription?.cancel();
}

/// Handle a local notification response when the app is in the foreground.
Future<void> onNotificationResponse(String? actionid, Challenge challenge) async {
Future<void> _onNotificationResponse(String? actionid, Challenge challenge) async {
final challengeId = challenge.id;

switch (actionid) {
Expand Down
43 changes: 40 additions & 3 deletions lib/src/model/correspondence/correspondence_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_g
import 'package:lichess_mobile/src/model/game/game_repository.dart';
import 'package:lichess_mobile/src/model/game/game_socket_events.dart';
import 'package:lichess_mobile/src/model/game/playable_game.dart';
import 'package:lichess_mobile/src/model/notifications/notification_service.dart';
import 'package:lichess_mobile/src/model/notifications/notifications.dart';
import 'package:lichess_mobile/src/navigation.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
Expand All @@ -27,7 +29,9 @@ part 'correspondence_service.g.dart';

@Riverpod(keepAlive: true)
CorrespondenceService correspondenceService(Ref ref) {
return CorrespondenceService(Logger('CorrespondenceService'), ref: ref);
final service = CorrespondenceService(Logger('CorrespondenceService'), ref: ref);
ref.onDispose(() => service.dispose());
return service;
}

/// Services that manages correspondence games.
Expand All @@ -37,8 +41,41 @@ class CorrespondenceService {
final Ref ref;
final Logger _log;

StreamSubscription<ParsedLocalNotification>? _notificationResponseSubscription;
StreamSubscription<ReceivedFcmMessage>? _fcmSubscription;

void start() {
_fcmSubscription = NotificationService.fcmMessageStream.listen((data) {
final (message: fcmMessage, fromBackground: fromBackground) = data;
switch (fcmMessage) {
case CorresGameUpdateFcmMessage(fullId: final fullId, game: final game):
if (game != null) {
_onServerUpdateEvent(fullId, game, fromBackground: fromBackground);
}

case _:
break;
}
});

_notificationResponseSubscription = NotificationService.responseStream.listen((data) {
final (_, notification) = data;
switch (notification) {
case CorresGameUpdateNotification(:final fullId):
_onNotificationResponse(fullId);
case _:
break;
}
});
}

void dispose() {
_fcmSubscription?.cancel();
_notificationResponseSubscription?.cancel();
}

/// Handles a notification response that caused the app to open.
Future<void> onNotificationResponse(GameFullId fullId) async {
Future<void> _onNotificationResponse(GameFullId fullId) async {
final context = ref.read(currentNavigatorKeyProvider).currentContext;
if (context == null || !context.mounted) return;

Expand Down Expand Up @@ -185,7 +222,7 @@ class CorrespondenceService {
}

/// Handles a game update event from the server.
Future<void> onServerUpdateEvent(
Future<void> _onServerUpdateEvent(
GameFullId fullId,
PlayableGame game, {
required bool fromBackground,
Expand Down
1 change: 1 addition & 0 deletions lib/src/model/game/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.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/account/account_service.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart';
import 'package:lichess_mobile/src/model/clock/chess_clock.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
Expand Down
Loading