Skip to content

Commit c54f395

Browse files
committed
Preset messages allow players (including anonymous non-logged in users) to send preset messages such as 'good luck' and 'have fun' on game start and 'gg' on game end.
This functionality mirrors that currently on the website. Limitations: * No internationalisation support (this is also the case on the website and would be a separate piece of work requiring updates to both the phone app and website to make the messages appear in both users' languages.) * Whether a preset has already been set can't be synced up with the website / other devices (I don't think this is a big deal :P.) * State forgotten when closing the app and returning to game Message Bubble Bug Fix Prior to this PR, the message bubble functionality didn't take into account that anonymous players can send these preset messages and the bubble 'else' case makes it look like it was the phone player who sent these even if it was the other player. I added a change to expose the "colour" field (`c`) in the websocket message. State Persistence The state is written to sqlite so it is retained when games are opened again after the app is closed (or the game is moved away from - I don't think this is possible at the moment but it perhaps could be in the future to navigate between multiple correspondence games if it isn't possible already.) If you think this is heavy handed let me know and I could explore just retaining it for the lifetime of the game widget. Being a flutter noob, I'd have to work out how to pipe that state together as the chat widget state is built from scratch each time the chat button is clicked and creates a 'router change event'. I thought putting it in SQLlite was probably fine because the chat widget does similar for new / read messages, etc. Scenarios Tested * Chat bubbles still work as expected after adding in the 'look at the anonymous player's colour if they're not logged in' change. * Clicking the preset buttons makes them disappear. * Pressing 2 buttons on either game start or end makes all the bubbles disappear. * The chat buttons move from the 'start' buttons to 'end' buttons when the game has ended in less than 4 moves * The 'start' chat buttons disappear after 4 moves have been played. * The 'end' buttons appear if the user clicks that chat after the game ends * The end buttons appear if the user is on the chat while the game ends * The start buttons disappear if the user clicks the chat after the 4th move * The start buttons disappear if the user is on the chat when the 4th move has been played
1 parent 2976187 commit c54f395

File tree

6 files changed

+234
-6
lines changed

6 files changed

+234
-6
lines changed

lib/src/model/game/chat_controller.dart

+4-5
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,9 @@ class ChatController extends _$ChatController {
126126
final data = event.data as Map<String, dynamic>;
127127
final message = data['t'] as String;
128128
final username = data['u'] as String?;
129+
final colour = data['c'] as String?;
129130
_addMessage(
130-
(
131-
message: message,
132-
username: username,
133-
),
131+
(message: message, username: username, colour: colour),
134132
);
135133
}
136134
}
@@ -146,11 +144,12 @@ class ChatState with _$ChatState {
146144
}) = _ChatState;
147145
}
148146

149-
typedef Message = ({String? username, String message});
147+
typedef Message = ({String? username, String? colour, String message});
150148

151149
Message _messageFromPick(RequiredPick pick) {
152150
return (
153151
message: pick('t').asStringOrThrow(),
154152
username: pick('u').asStringOrNull(),
153+
colour: pick('c').asStringOrNull(),
155154
);
156155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
import 'package:lichess_mobile/src/model/common/id.dart';
3+
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
4+
import 'package:lichess_mobile/src/model/game/game_controller.dart';
5+
import 'package:lichess_mobile/src/model/game/message_presets.dart';
6+
import 'package:riverpod_annotation/riverpod_annotation.dart';
7+
8+
part 'chat_presets_controller.freezed.dart';
9+
part 'chat_presets_controller.g.dart';
10+
11+
@riverpod
12+
class ChatPresetsController extends _$ChatPresetsController {
13+
late GameFullId _gameId;
14+
15+
static const Map<PresetMessageGroup, List<PresetMessage>> _presetMessages = {
16+
PresetMessageGroup.start: [
17+
(label: 'HI', value: 'Hello'),
18+
(label: 'GL', value: 'Good luck'),
19+
(label: 'HF', value: 'Have fun!'),
20+
(label: 'U2', value: 'You too!'),
21+
],
22+
PresetMessageGroup.end: [
23+
(label: 'GG', value: 'Good game'),
24+
(label: 'WP', value: 'Well played'),
25+
(label: 'TY', value: 'Thank you'),
26+
(label: 'GTG', value: "I've got to go"),
27+
(label: 'BYE', value: 'Bye!'),
28+
],
29+
};
30+
31+
@override
32+
Future<ChatPresetsState> build(GameFullId id) async {
33+
_gameId = id;
34+
35+
final gameState = ref.read(gameControllerProvider(_gameId)).value;
36+
37+
ref.listen(gameControllerProvider(_gameId), _handleGameStateChange);
38+
39+
if (gameState != null) {
40+
final presetMessageGroup = PresetMessageGroup.fromGame(gameState.game);
41+
42+
const List<PresetMessage> alreadySaid = [];
43+
44+
final initialState = ChatPresetsState(
45+
presets: _presetMessages,
46+
gameId: _gameId,
47+
alreadySaid: alreadySaid,
48+
currentPresetMessageGroup: presetMessageGroup,
49+
);
50+
51+
return initialState;
52+
} else {
53+
return ChatPresetsState(
54+
presets: _presetMessages,
55+
gameId: _gameId,
56+
alreadySaid: [],
57+
currentPresetMessageGroup: null,
58+
);
59+
}
60+
}
61+
62+
void _handleGameStateChange(
63+
AsyncValue<GameState>? previousGame,
64+
AsyncValue<GameState> currentGame,
65+
) {
66+
final newGameState = currentGame.value;
67+
68+
if (newGameState != null) {
69+
final newMessageGroup = PresetMessageGroup.fromGame(newGameState.game);
70+
71+
final currentMessageGroup = state.value?.currentPresetMessageGroup;
72+
73+
if (newMessageGroup != currentMessageGroup) {
74+
state = state.whenData((s) {
75+
final newState = s.copyWith(
76+
currentPresetMessageGroup: newMessageGroup,
77+
alreadySaid: [],
78+
);
79+
80+
return newState;
81+
});
82+
}
83+
}
84+
}
85+
86+
void sendPreset(PresetMessage message) {
87+
final chatController = ref.read(chatControllerProvider(_gameId).notifier);
88+
chatController.sendMessage(message.value);
89+
90+
state = state.whenData((s) {
91+
final state = s.copyWith(alreadySaid: [...s.alreadySaid, message]);
92+
return state;
93+
});
94+
}
95+
}
96+
97+
@freezed
98+
class ChatPresetsState with _$ChatPresetsState {
99+
const ChatPresetsState._();
100+
101+
const factory ChatPresetsState({
102+
required Map<PresetMessageGroup, List<PresetMessage>> presets,
103+
required GameFullId gameId,
104+
required List<PresetMessage> alreadySaid,
105+
required PresetMessageGroup? currentPresetMessageGroup,
106+
}) = _ChatPresetsState;
107+
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:lichess_mobile/src/model/game/game_status.dart';
2+
import 'package:lichess_mobile/src/model/game/playable_game.dart';
3+
4+
enum PresetMessageGroup {
5+
start,
6+
end;
7+
8+
static PresetMessageGroup? fromString(String groupName) {
9+
if (groupName == 'start') {
10+
return start;
11+
} else if (groupName == 'end') {
12+
return end;
13+
} else {
14+
return null;
15+
}
16+
}
17+
18+
static PresetMessageGroup? fromGame(PlayableGame game) {
19+
if (game.status.value <= GameStatus.mate.value && game.steps.length < 4) {
20+
return start;
21+
} else if (game.status.value >= GameStatus.mate.value) {
22+
return end;
23+
} else {
24+
return null;
25+
}
26+
}
27+
}
28+
29+
typedef PresetMessage = ({String label, String value});

lib/src/view/game/game_body.dart

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart';
1212
import 'package:lichess_mobile/src/model/common/id.dart';
1313
import 'package:lichess_mobile/src/model/common/speed.dart';
1414
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
15+
import 'package:lichess_mobile/src/model/game/chat_presets_controller.dart';
1516
import 'package:lichess_mobile/src/model/game/game_controller.dart';
1617
import 'package:lichess_mobile/src/model/game/game_preferences.dart';
1718
import 'package:lichess_mobile/src/model/game/playable_game.dart';
@@ -424,6 +425,8 @@ class _GameBottomBar extends ConsumerWidget {
424425
? ref.watch(chatControllerProvider(id))
425426
: null;
426427

428+
_keepChatPresetsState(ref);
429+
427430
final List<Widget> children = gameStateAsync.when(
428431
data: (gameState) {
429432
final isChatEnabled =
@@ -861,6 +864,15 @@ class _GameBottomBar extends ConsumerWidget {
861864
onConfirm();
862865
}
863866
}
867+
868+
void _keepChatPresetsState(WidgetRef ref) {
869+
// By listening to the chat presets state we keep it available for the lifetime of the game.
870+
// If we didn't do this, it would be lost each time the chat is closed
871+
ref.listen(
872+
chatPresetsControllerProvider(id),
873+
(previous, next) => {},
874+
);
875+
}
864876
}
865877

866878
class _GameNegotiationDialog extends StatelessWidget {

lib/src/view/game/message_screen.dart

+27-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
44
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
55
import 'package:lichess_mobile/src/model/common/id.dart';
66
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
7+
import 'package:lichess_mobile/src/model/game/chat_presets_controller.dart';
8+
import 'package:lichess_mobile/src/model/game/game_controller.dart';
79
import 'package:lichess_mobile/src/model/settings/brightness.dart';
810
import 'package:lichess_mobile/src/model/user/user.dart';
911
import 'package:lichess_mobile/src/navigation.dart';
1012
import 'package:lichess_mobile/src/styles/lichess_colors.dart';
1113
import 'package:lichess_mobile/src/styles/styles.dart';
1214
import 'package:lichess_mobile/src/utils/l10n_context.dart';
15+
import 'package:lichess_mobile/src/view/game/preset_messages.dart';
1316
import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart';
1417
import 'package:lichess_mobile/src/widgets/buttons.dart';
1518
import 'package:lichess_mobile/src/widgets/platform.dart';
@@ -99,7 +102,13 @@ class _Body extends ConsumerWidget {
99102

100103
@override
101104
Widget build(BuildContext context, WidgetRef ref) {
105+
final presetsController = chatPresetsControllerProvider(id);
102106
final chatStateAsync = ref.watch(chatControllerProvider(id));
107+
final gameStateAsync = ref.watch(gameControllerProvider(id));
108+
final chatPresetsStateAsync = ref.watch(presetsController);
109+
110+
final myColour = gameStateAsync.value?.game.youAre;
111+
final chatPresetState = chatPresetsStateAsync.value;
103112

104113
return Column(
105114
mainAxisSize: MainAxisSize.max,
@@ -118,9 +127,14 @@ class _Body extends ConsumerWidget {
118127
itemBuilder: (context, index) {
119128
final message =
120129
chatState.messages[chatState.messages.length - index - 1];
130+
131+
final isMyMessage = message.username != null
132+
? message.username == me?.name
133+
: (message.colour == myColour?.name);
134+
121135
return (message.username == 'lichess')
122136
? _MessageAction(message: message.message)
123-
: (message.username == me?.name)
137+
: isMyMessage
124138
? _MessageBubble(
125139
you: true,
126140
message: message.message,
@@ -140,6 +154,18 @@ class _Body extends ConsumerWidget {
140154
),
141155
),
142156
),
157+
// Only show presets if the player is participating in the game and the presets state has become available
158+
if (myColour != null && chatPresetState != null)
159+
PresetMessages(
160+
gameId: id,
161+
alreadySaid: chatPresetState.alreadySaid,
162+
presetMessageGroup: chatPresetState.currentPresetMessageGroup,
163+
presetMessages: chatPresetState.presets,
164+
sendChatPreset: (presetMessage) =>
165+
ref.read(presetsController.notifier).sendPreset(presetMessage),
166+
)
167+
else
168+
const SizedBox.shrink(),
143169
_ChatBottomBar(id: id),
144170
],
145171
);
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:lichess_mobile/src/model/common/id.dart';
4+
import 'package:lichess_mobile/src/model/game/message_presets.dart';
5+
import 'package:lichess_mobile/src/widgets/buttons.dart';
6+
7+
class PresetMessages extends ConsumerWidget {
8+
final GameFullId gameId;
9+
final List<PresetMessage> alreadySaid;
10+
final PresetMessageGroup? presetMessageGroup;
11+
final Map<PresetMessageGroup, List<PresetMessage>> presetMessages;
12+
final void Function(PresetMessage presetMessage) sendChatPreset;
13+
14+
const PresetMessages({
15+
required this.gameId,
16+
required this.alreadySaid,
17+
required this.presetMessageGroup,
18+
required this.presetMessages,
19+
required this.sendChatPreset,
20+
});
21+
22+
@override
23+
Widget build(BuildContext context, WidgetRef ref) {
24+
final messages = presetMessages[presetMessageGroup] ?? [];
25+
26+
if (messages.isEmpty || alreadySaid.length >= 2) {
27+
return const SizedBox.shrink();
28+
}
29+
30+
final notAlreadySaid =
31+
messages.where((message) => !alreadySaid.contains(message));
32+
33+
return Row(
34+
children: notAlreadySaid
35+
.map((preset) => _renderPresetMessageButton(preset, ref))
36+
.toList(),
37+
);
38+
}
39+
40+
Widget _renderPresetMessageButton(PresetMessage preset, WidgetRef ref) {
41+
return Padding(
42+
padding: const EdgeInsets.all(4.0),
43+
child: SecondaryButton(
44+
semanticsLabel: preset.label,
45+
onPressed: () {
46+
sendChatPreset(preset);
47+
},
48+
child: Text(
49+
preset.label,
50+
textAlign: TextAlign.center,
51+
),
52+
),
53+
);
54+
}
55+
}

0 commit comments

Comments
 (0)