Skip to content

Commit 0b52b69

Browse files
committed
feat: support creating challenge links
Closes #2015 Closes #856
1 parent b2e6103 commit 0b52b69

18 files changed

+505
-154
lines changed

lib/src/model/challenge/challenge.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ sealed class ChallengeRequest with _$ChallengeRequest, BaseChallenge implements
111111
const ChallengeRequest._();
112112

113113
const factory ChallengeRequest({
114-
required LightUser destUser,
114+
// If null, it's an open challenge that anyone (even anonymous users) can accept.
115+
LightUser? destUser,
115116
required Variant variant,
116117
required ChallengeTimeControlType timeControl,
117118
({Duration time, Duration increment})? clock,

lib/src/model/challenge/challenge_preferences.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ sealed class ChallengePrefs with _$ChallengePrefs implements Serializable {
9090
? variant != Variant.fromPosition
9191
: timeControl == ChallengeTimeControlType.correspondence && variant == Variant.standard;
9292

93-
ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) {
93+
ChallengeRequest makeRequest(LightUser? destUser, [String? initialFen]) {
9494
return ChallengeRequest(
9595
destUser: destUser,
9696
variant: variant,

lib/src/model/challenge/challenge_repository.dart

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import 'dart:async';
2+
import 'dart:math';
23

4+
import 'package:dartchess/dartchess.dart';
35
import 'package:deep_pick/deep_pick.dart';
46
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
57
import 'package:flutter_riverpod/flutter_riverpod.dart';
68
import 'package:lichess_mobile/src/model/challenge/challenge.dart';
9+
import 'package:lichess_mobile/src/model/common/game.dart';
710
import 'package:lichess_mobile/src/model/common/id.dart';
811
import 'package:lichess_mobile/src/network/aggregator.dart';
912
import 'package:lichess_mobile/src/network/http.dart';
@@ -40,17 +43,33 @@ class ChallengeRepository {
4043
return client.readJson(uri, mapper: Challenge.fromServerJson);
4144
}
4245

43-
Future<Challenge> create(ChallengeRequest challenge) {
44-
final uri = Uri(path: '/api/challenge/${challenge.destUser.id}');
45-
return client.postReadJson(
46+
Future<Challenge> create(ChallengeRequest challengeReq) async {
47+
final uri = Uri(path: '/api/challenge/${challengeReq.destUser?.id ?? 'open'}');
48+
final challenge = await client.postReadJson(
4649
uri,
47-
body: challenge.toRequestBody,
50+
body: challengeReq.toRequestBody,
4851
mapper: Challenge.fromServerJson,
4952
);
53+
54+
// The API doesn't directly allow us to create an open challenge and also join it in one step,
55+
// so after having created the challenge, we immediately accept it if it's an open challenge.
56+
if (challengeReq.destUser == null) {
57+
final side = switch (challengeReq.sideChoice) {
58+
SideChoice.white => Side.white,
59+
SideChoice.black => Side.black,
60+
SideChoice.random => Side.values[Random().nextInt(Side.values.length)],
61+
};
62+
await accept(challenge.id, side: side);
63+
}
64+
65+
return challenge;
5066
}
5167

52-
Future<void> accept(ChallengeId id) async {
53-
final uri = Uri(path: '/api/challenge/$id/accept');
68+
Future<void> accept(ChallengeId id, {Side? side}) async {
69+
final uri = Uri(
70+
path: '/api/challenge/$id/accept',
71+
queryParameters: {if (side != null) 'color': side.name},
72+
);
5473
await client.postRead(uri);
5574
}
5675

lib/src/model/lobby/create_game_service.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,29 @@ class CreateGameService {
119119
}
120120

121121
/// Create a new real time challenge.
122-
///
123-
/// Will listen to the challenge socket and await the response from the destinated user.
124-
Future<ChallengeResponse> newRealTimeChallenge(ChallengeRequest challengeReq) async {
122+
Future<Challenge> newRealTimeChallenge(ChallengeRequest challengeReq) async {
125123
assert(challengeReq.timeControl == ChallengeTimeControlType.clock);
126124

127125
if (_challengeConnection != null) {
128126
throw StateError('Already creating a challenge.');
129127
}
130128

129+
return await challengeRepository.create(challengeReq);
130+
}
131+
132+
/// Wait for a response to a [challenge].
133+
///
134+
/// Will listen to the challenge socket and await the response from the destinated user.
135+
Future<ChallengeResponse> waitForChallengeResponse(Challenge challenge) {
136+
assert(challenge.timeControl == ChallengeTimeControlType.clock);
137+
if (_challengeConnection != null) {
138+
throw StateError('Already creating a challenge.');
139+
}
140+
131141
// ensure the pending connection is closed in any case
132142
final completer = Completer<ChallengeResponse>()..future.whenComplete(dispose);
133143

134144
try {
135-
_log.info('Creating new challenge game');
136-
137-
final challenge = await challengeRepository.create(challengeReq);
138-
139145
final socketPool = ref.read(socketPoolProvider);
140146
final socketClient = socketPool.open(
141147
Uri(

lib/src/view/board_editor/board_editor_screen.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ class _BottomBar extends ConsumerWidget {
347347
isScrollControlled: true,
348348
useRootNavigator: true,
349349
builder: (context) {
350-
return CreateChallengeBottomSheet(user, positionFen: editorState.fen);
350+
return CreateChallengeBottomSheet(
351+
user: user,
352+
positionFen: editorState.fen,
353+
);
351354
},
352355
);
353356
},

0 commit comments

Comments
 (0)