Skip to content

Commit 12a2d57

Browse files
committed
feat: add "or invite a user" to "share link" screen
1 parent 93358ab commit 12a2d57

File tree

8 files changed

+278
-58
lines changed

8 files changed

+278
-58
lines changed

lib/src/model/lobby/create_game_service.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,13 @@ class CreateGameService {
196196

197197
/// Create a new correspondence challenge.
198198
///
199-
/// It will wait a little bit in case of an immediate decline (e.g. bots), in that case a
199+
/// If [waitForImmediateDecline] is true, will wait a little bit in case of an immediate decline (e.g. bots), in that case a
200200
/// [ChallengeDeclineReason] will be returned.
201201
/// Otherwise, it means the challenge was created successfully.
202-
Future<ChallengeDeclineReason?> newCorrespondenceChallenge(ChallengeRequest challengeReq) async {
202+
Future<ChallengeDeclineReason?> newCorrespondenceChallenge(
203+
ChallengeRequest challengeReq, {
204+
bool waitForImmediateDecline = true,
205+
}) async {
203206
assert(
204207
challengeReq.timeControl == ChallengeTimeControlType.correspondence ||
205208
challengeReq.timeControl == ChallengeTimeControlType.unlimited,

lib/src/view/board_editor/board_editor_screen.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_positions.dart
2222
import 'package:lichess_mobile/src/view/offline_computer/offline_computer_game_screen.dart';
2323
import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart';
2424
import 'package:lichess_mobile/src/view/play/create_challenge_bottom_sheet.dart';
25-
import 'package:lichess_mobile/src/view/user/search_screen.dart';
25+
import 'package:lichess_mobile/src/view/user/pick_player_screen.dart';
2626
import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
2727
import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart';
2828
import 'package:lichess_mobile/src/widgets/bottom_bar.dart';
@@ -332,7 +332,7 @@ class _BottomBar extends ConsumerWidget {
332332
return;
333333
}
334334
Navigator.of(context).push(
335-
SearchScreen.buildRoute(
335+
PickPlayerScreen.buildRoute(
336336
context,
337337
onUserTap: (user) {
338338
if (user.id == authUser.user.id) {

lib/src/view/game/game_loading_board.dart

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
55
import 'package:flutter_riverpod/flutter_riverpod.dart';
66
import 'package:lichess_mobile/src/constants.dart';
7+
import 'package:lichess_mobile/src/model/auth/auth_controller.dart';
78
import 'package:lichess_mobile/src/model/challenge/challenge.dart';
9+
import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart';
810
import 'package:lichess_mobile/src/model/common/chess.dart';
11+
import 'package:lichess_mobile/src/model/common/id.dart';
912
import 'package:lichess_mobile/src/model/game/game_board_params.dart';
1013
import 'package:lichess_mobile/src/model/lobby/game_seek.dart';
1114
import 'package:lichess_mobile/src/model/lobby/lobby_numbers.dart';
@@ -15,6 +18,9 @@ import 'package:lichess_mobile/src/utils/share.dart';
1518
import 'package:lichess_mobile/src/utils/string.dart';
1619
import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart';
1720
import 'package:lichess_mobile/src/view/game/game_body.dart';
21+
import 'package:lichess_mobile/src/view/game/game_screen.dart';
22+
import 'package:lichess_mobile/src/view/game/game_screen_providers.dart';
23+
import 'package:lichess_mobile/src/view/user/pick_player_screen.dart';
1824
import 'package:lichess_mobile/src/widgets/bottom_bar.dart';
1925
import 'package:lichess_mobile/src/widgets/feedback.dart';
2026
import 'package:lichess_mobile/src/widgets/game_layout.dart';
@@ -211,9 +217,14 @@ class _UserChallengeLoadingContentState extends State<UserChallengeLoadingConten
211217
}
212218

213219
class OpenChallengeLoadingContent extends ConsumerStatefulWidget {
214-
const OpenChallengeLoadingContent(this.challenge, this.cancelChallenge);
215-
216-
final Challenge challenge;
220+
const OpenChallengeLoadingContent({
221+
required this.id,
222+
required this.challengeRequest,
223+
required this.cancelChallenge,
224+
});
225+
226+
final ChallengeId id;
227+
final ChallengeRequest challengeRequest;
217228
final Future<void> Function() cancelChallenge;
218229

219230
@override
@@ -225,7 +236,8 @@ class _OpenChallengeLoadingContentState extends ConsumerState<OpenChallengeLoadi
225236

226237
@override
227238
Widget build(BuildContext context) {
228-
final challengeLink = 'https://$kLichessHost/${widget.challenge.id}';
239+
final challengeLink = 'https://$kLichessHost/${widget.id}';
240+
final authUser = ref.watch(authControllerProvider);
229241

230242
final qrColor = ref.watch(currentBrightnessProvider) == Brightness.dark
231243
? Colors.white
@@ -252,13 +264,13 @@ class _OpenChallengeLoadingContentState extends ConsumerState<OpenChallengeLoadi
252264
mainAxisSize: MainAxisSize.min,
253265
children: [
254266
Icon(
255-
widget.challenge.perf.icon,
267+
widget.challengeRequest.perf.icon,
256268
color: DefaultTextStyle.of(context).style.color,
257269
),
258270
const SizedBox(width: 8.0),
259271
Text(
260-
widget.challenge.timeIncrement?.display ??
261-
'${context.l10n.daysPerTurn}: ${widget.challenge.days}',
272+
widget.challengeRequest.timeIncrement?.display ??
273+
'${context.l10n.daysPerTurn}: ${widget.challengeRequest.days}',
262274
style: TextTheme.of(context).titleLarge,
263275
),
264276
],
@@ -295,6 +307,47 @@ class _OpenChallengeLoadingContentState extends ConsumerState<OpenChallengeLoadi
295307

296308
Text(context.l10n.theFirstPersonToComeOnThisUrlWillPlayWithYou),
297309

310+
if (authUser != null)
311+
FilledButton.tonalIcon(
312+
onPressed: () => Navigator.of(context).push(
313+
PickPlayerScreen.buildRoute(
314+
context,
315+
title: Text(context.l10n.challengeAFriend),
316+
onUserTap: (user) async {
317+
Navigator.of(context).pop();
318+
319+
widget.cancelChallenge();
320+
321+
final directedChallengeReq = widget.challengeRequest.copyWith(
322+
destUser: user,
323+
);
324+
await ref
325+
.read(challengeRepositoryProvider)
326+
.create(directedChallengeReq);
327+
if (!context.mounted) return;
328+
329+
if (widget.challengeRequest.timeControl ==
330+
ChallengeTimeControlType.clock) {
331+
Navigator.of(context, rootNavigator: true).pushReplacement(
332+
GameScreen.buildRoute(
333+
context,
334+
source: UserChallengeSource(directedChallengeReq),
335+
),
336+
);
337+
} else {
338+
if (!context.mounted) return;
339+
ScaffoldMessenger.of(context).showSnackBar(
340+
SnackBar(content: Text(context.l10n.mobileChallengeCreated)),
341+
);
342+
Navigator.of(context).pop();
343+
}
344+
},
345+
),
346+
),
347+
label: Text(context.l10n.challengeInviteLichessUser.replaceFirst(':', '')),
348+
icon: const Icon(Icons.person_search),
349+
),
350+
298351
Container(
299352
padding: const EdgeInsets.all(4.0),
300353
decoration: BoxDecoration(

lib/src/view/game/game_screen.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,9 @@ class _GameScreenState extends ConsumerState<GameScreen> {
231231
body: PopScope(
232232
canPop: false,
233233
child: OpenChallengeLoadingContent(
234-
challenge,
235-
ref.read(createGameServiceProvider).cancelChallenge,
234+
id: challenge.id,
235+
challengeRequest: (widget.source as UserChallengeSource).challengeRequest,
236+
cancelChallenge: ref.read(createGameServiceProvider).cancelChallenge,
236237
),
237238
),
238239
);

lib/src/view/play/create_challenge_bottom_sheet.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ class _CreateChallengeBottomSheetState extends ConsumerState<CreateChallengeBott
403403
: null,
404404
child: Text(
405405
widget.user != null
406-
? context.l10n.challengeChallengeToPlay
406+
? context.l10n.challengeX(widget.user!.name)
407407
: context.l10n.challengeAFriend,
408408
style: Styles.bold,
409409
),
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:lichess_mobile/src/model/user/user.dart';
4+
import 'package:lichess_mobile/src/utils/l10n_context.dart';
5+
import 'package:lichess_mobile/src/utils/navigation.dart';
6+
import 'package:lichess_mobile/src/view/relation/friend_screen.dart';
7+
import 'package:lichess_mobile/src/view/user/search_screen.dart';
8+
import 'package:lichess_mobile/src/widgets/list.dart';
9+
import 'package:lichess_mobile/src/widgets/platform.dart';
10+
import 'package:lichess_mobile/src/widgets/platform_search_bar.dart';
11+
import 'package:lichess_mobile/src/widgets/shimmer.dart';
12+
import 'package:lichess_mobile/src/widgets/user.dart';
13+
14+
class PickPlayerScreen extends ConsumerWidget {
15+
const PickPlayerScreen({required this.onUserTap, required this.title, super.key});
16+
17+
final void Function(LightUser) onUserTap;
18+
final Widget title;
19+
20+
static Route<dynamic> buildRoute(
21+
BuildContext context, {
22+
required void Function(LightUser) onUserTap,
23+
required Widget title,
24+
}) {
25+
return buildScreenRoute(
26+
context,
27+
screen: PickPlayerScreen(onUserTap: onUserTap, title: title),
28+
);
29+
}
30+
31+
@override
32+
Widget build(BuildContext context, WidgetRef ref) {
33+
return PlatformScaffold(
34+
appBar: PlatformAppBar(title: title),
35+
body: _Body(onUserTap: onUserTap),
36+
);
37+
}
38+
}
39+
40+
class _Body extends ConsumerWidget {
41+
const _Body({required this.onUserTap});
42+
43+
final void Function(LightUser) onUserTap;
44+
45+
@override
46+
Widget build(BuildContext context, WidgetRef ref) {
47+
switch (ref.watch(followingStatusesProvider)) {
48+
case AsyncData(value: (final following, final statuses)):
49+
final online = <User>[];
50+
final offline = <User>[];
51+
for (final (user, status) in following.zip(statuses)) {
52+
if (status.online == true) {
53+
online.add(user);
54+
} else {
55+
offline.add(user);
56+
}
57+
}
58+
return _PlayersList(
59+
onUserTap: onUserTap,
60+
children: [
61+
if (online.isNotEmpty)
62+
ListSection(
63+
header: Text(context.l10n.nbFriendsOnline(online.length)),
64+
children: [
65+
for (final user in online)
66+
_FriendTile(friend: user.lightUser, onUserTap: onUserTap),
67+
],
68+
),
69+
if (offline.isNotEmpty)
70+
ListSection(
71+
header: Text(context.l10n.following),
72+
children: [
73+
for (final user in offline)
74+
_FriendTile(friend: user.lightUser, onUserTap: onUserTap),
75+
],
76+
),
77+
],
78+
);
79+
case _:
80+
return _PlayersList(
81+
onUserTap: onUserTap,
82+
children: [
83+
Shimmer(
84+
child: ShimmerLoading(isLoading: true, child: ListSection.loading(itemsNumber: 5)),
85+
),
86+
Shimmer(
87+
child: ShimmerLoading(isLoading: true, child: ListSection.loading(itemsNumber: 5)),
88+
),
89+
],
90+
);
91+
}
92+
}
93+
}
94+
95+
class _PlayersList extends StatelessWidget {
96+
const _PlayersList({required this.onUserTap, required this.children});
97+
98+
final void Function(LightUser) onUserTap;
99+
final List<Widget> children;
100+
101+
@override
102+
Widget build(BuildContext context) {
103+
return ListView(
104+
children: [
105+
Padding(
106+
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
107+
child: PlatformSearchBar(
108+
hintText: context.l10n.searchSearch,
109+
focusNode: _AlwaysDisabledFocusNode(),
110+
onTap: () => Navigator.of(context).push(
111+
SearchScreen.buildRoute(
112+
context,
113+
onUserTap: (user) {
114+
Navigator.of(context).pop();
115+
onUserTap(user);
116+
},
117+
),
118+
),
119+
),
120+
),
121+
...children,
122+
],
123+
);
124+
}
125+
}
126+
127+
class _AlwaysDisabledFocusNode extends FocusNode {
128+
@override
129+
bool get hasFocus => false;
130+
}
131+
132+
class _FriendTile extends StatelessWidget {
133+
const _FriendTile({required this.friend, required this.onUserTap});
134+
135+
final LightUser friend;
136+
final void Function(LightUser) onUserTap;
137+
138+
@override
139+
Widget build(BuildContext context) {
140+
return ListTile(
141+
title: UserFullNameWidget(user: friend),
142+
onTap: () => onUserTap(friend),
143+
);
144+
}
145+
}

lib/src/view/user/search_screen.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
6666
_term = term;
6767
});
6868
saveHistoryDebouncer.call(() {
69+
if (!mounted) return;
6970
ref.read(searchHistoryProvider.notifier).saveTerm(term);
7071
});
7172
} else {

0 commit comments

Comments
 (0)