Skip to content
Open
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
24 changes: 16 additions & 8 deletions lib/src/model/challenge/challenge_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,22 @@ class ChallengeService {
);

// new incoming challenges
await Future.wait(
_current?.inward.whereNot((challenge) => prevInwardIds.contains(challenge.id)).map((
challenge,
) async {
return await notificationService.show(ChallengeNotification(challenge));
}) ??
<Future<int>>[],
);
// only display the notifications if the app is in the foregroud because fcm already
// shows notifications when the app is in the background
// TODO find a better solution to avoid duplicate notifications
final state = WidgetsBinding.instance.lifecycleState;
final isForeground =
state == null || state == AppLifecycleState.resumed || state == AppLifecycleState.inactive;
if (isForeground) {
await Future.wait(
_current?.inward.whereNot((challenge) => prevInwardIds.contains(challenge.id)).map((
challenge,
) async {
return await notificationService.show(ChallengeNotification(challenge));
}) ??
<Future<int>>[],
);
}
}

/// Stop listening to challenge events from the server.
Expand Down
4 changes: 2 additions & 2 deletions lib/src/model/notifications/notifications.dart
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,8 @@ class ChallengeAcceptedNotification extends LocalNotification {

/// A notification for a challenge creation.
///
/// This notification is shown when a challenge is created on the server while the user is not connected to lichess (e.g., app is in background).
/// If the user is connected, challenges are handled by Websocket and a [ChallengeNotification] is shown instead.
/// This notification is shown when a challenge is created on the server while the app is in the background.
/// If the app is in the foreground, challenges are handled by WebSocket and a [ChallengeNotification] is shown instead.
class ChallengeCreatedNotification extends LocalNotification {
const ChallengeCreatedNotification(this.challengeId, String title, String body)
: _title = title,
Expand Down
71 changes: 70 additions & 1 deletion test/model/challenge/challenge_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class _ShowDeclineDialogWidget extends ConsumerWidget {
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final binding = TestWidgetsFlutterBinding.ensureInitialized();

setUpAll(() {
registerFallbackValue(const ChallengeId(''));
Expand All @@ -73,6 +73,8 @@ void main() {

tearDown(() {
reset(notificationDisplayMock);
// reset lifecycle state between tests
binding.resetInternalState();
});

test('exposes a challenges stream', () async {
Expand Down Expand Up @@ -124,6 +126,9 @@ void main() {
});

test('Listen to socket and show a notification for any new challenge', () async {
// notifications from socket are only displayed if app is in foreground
binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);

when(
() => notificationDisplayMock.show(
id: any(named: 'id'),
Expand Down Expand Up @@ -208,7 +213,71 @@ void main() {
});
});

test('Does not show local notification when app is in background', () async {
// when app is in background, socket notifications should not be displayed
binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);

when(
() => notificationDisplayMock.show(
id: any(named: 'id'),
title: any(named: 'title'),
body: any(named: 'body'),
notificationDetails: any(named: 'notificationDetails'),
payload: any(named: 'payload'),
),
).thenAnswer((_) => Future.value());

final container = await makeContainer(
authUser: fakeAuthUser,
overrides: {
notificationDisplayProvider: notificationDisplayProvider.overrideWithValue(
notificationDisplayMock,
),
},
);

final notificationService = container.read(notificationServiceProvider);
final challengeService = container.read(challengeServiceProvider);

fakeAsync((async) {
final socketClient = makeTestSocketClient();
socketClient.connect();
notificationService.start();
challengeService.start();

// wait for the socket to connect
async.elapse(const Duration(milliseconds: 100));
async.flushMicrotasks();

sendServerSocketMessages(Uri(path: kDefaultSocketRoute), [
'''
{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 }
''',
]);

async.flushMicrotasks();

// ensure no notification is shown while app is backgrounded
verifyNever(
() => notificationDisplayMock.show(
id: any(named: 'id'),
title: any(named: 'title'),
body: any(named: 'body'),
notificationDetails: any(named: 'notificationDetails'),
payload: any(named: 'payload'),
),
);

// closing the socket client to be able to flush the timers
socketClient.close();
async.flushTimers();
});
});

test('Cancels the notification for any missing challenge', () async {
// notifications from socket are only displayed if app is in foreground
binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);

when(
() => notificationDisplayMock.show(
id: any(named: 'id'),
Expand Down