Skip to content

Commit

Permalink
Preset messages allow players (including anonymous non-logged in user…
Browse files Browse the repository at this point in the history
…s) 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
  • Loading branch information
Happy0 committed Jul 4, 2024
1 parent 2976187 commit 4390ef4
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 6 deletions.
9 changes: 4 additions & 5 deletions lib/src/model/game/chat_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,9 @@ class ChatController extends _$ChatController {
final data = event.data as Map<String, dynamic>;
final message = data['t'] as String;
final username = data['u'] as String?;
final colour = data['c'] as String?;
_addMessage(
(
message: message,
username: username,
),
(message: message, username: username, colour: colour),
);
}
}
Expand All @@ -146,11 +144,12 @@ class ChatState with _$ChatState {
}) = _ChatState;
}

typedef Message = ({String? username, String message});
typedef Message = ({String? username, String? colour, String message});

Message _messageFromPick(RequiredPick pick) {
return (
message: pick('t').asStringOrThrow(),
username: pick('u').asStringOrNull(),
colour: pick('c').asStringOrNull(),
);
}
107 changes: 107 additions & 0 deletions lib/src/model/game/chat_presets_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
import 'package:lichess_mobile/src/model/game/game_controller.dart';
import 'package:lichess_mobile/src/model/game/message_presets.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'chat_presets_controller.freezed.dart';
part 'chat_presets_controller.g.dart';

@riverpod
class ChatPresetsController extends _$ChatPresetsController {
late GameFullId _gameId;

static const Map<PresetMessageGroup, List<PresetMessage>> _presetMessages = {
PresetMessageGroup.start: [
(label: 'HI', value: 'Hello'),
(label: 'GL', value: 'Good luck'),
(label: 'HF', value: 'Have fun!'),
(label: 'U2', value: 'You too!'),
],
PresetMessageGroup.end: [
(label: 'GG', value: 'Good game'),
(label: 'WP', value: 'Well played'),
(label: 'TY', value: 'Thank you'),
(label: 'GTG', value: "I've got to go"),
(label: 'BYE', value: 'Bye!'),
],
};

@override
Future<ChatPresetsState> build(GameFullId id) async {
_gameId = id;

final gameState = ref.read(gameControllerProvider(_gameId)).value;

ref.listen(gameControllerProvider(_gameId), _handleGameStateChange);

if (gameState != null) {
final presetMessageGroup = PresetMessageGroup.fromGame(gameState.game);

const List<PresetMessage> alreadySaid = [];

final initialState = ChatPresetsState(
presets: _presetMessages,
gameId: _gameId,
alreadySaid: alreadySaid,
currentPresetMessageGroup: presetMessageGroup,
);

return initialState;
} else {
return ChatPresetsState(
presets: _presetMessages,
gameId: _gameId,
alreadySaid: [],
currentPresetMessageGroup: null,
);
}
}

void _handleGameStateChange(
AsyncValue<GameState>? previousGame,
AsyncValue<GameState> currentGame,
) {
final newGameState = currentGame.value;

if (newGameState != null) {
final newMessageGroup = PresetMessageGroup.fromGame(newGameState.game);

final currentMessageGroup = state.value?.currentPresetMessageGroup;

if (newMessageGroup != currentMessageGroup) {
state = state.whenData((s) {
final newState = s.copyWith(
currentPresetMessageGroup: newMessageGroup,
alreadySaid: [],
);

return newState;
});
}
}
}

void sendPreset(PresetMessage message) {
final chatController = ref.read(chatControllerProvider(gameId).notifier);
chatController.sendMessage(message.value);

state = state.whenData((s) {
final state = s.copyWith(alreadySaid: [...s.alreadySaid, message]);
return state;
});
}
}

@freezed
class ChatPresetsState with _$ChatPresetsState {
const ChatPresetsState._();

const factory ChatPresetsState({
required Map<PresetMessageGroup, List<PresetMessage>> presets,
required GameFullId gameId,
required List<PresetMessage> alreadySaid,
required PresetMessageGroup? currentPresetMessageGroup,
}) = _ChatPresetsState;
}
29 changes: 29 additions & 0 deletions lib/src/model/game/message_presets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:lichess_mobile/src/model/game/game_status.dart';
import 'package:lichess_mobile/src/model/game/playable_game.dart';

enum PresetMessageGroup {
start,
end;

static PresetMessageGroup? fromString(String groupName) {
if (groupName == 'start') {
return start;
} else if (groupName == 'end') {
return end;
} else {
return null;
}
}

static PresetMessageGroup? fromGame(PlayableGame game) {
if (game.status.value <= GameStatus.mate.value && game.steps.length < 4) {
return start;
} else if (game.status.value >= GameStatus.mate.value) {
return end;
} else {
return null;
}
}
}

typedef PresetMessage = ({String label, String value});
12 changes: 12 additions & 0 deletions lib/src/view/game/game_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/common/speed.dart';
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
import 'package:lichess_mobile/src/model/game/chat_presets_controller.dart';
import 'package:lichess_mobile/src/model/game/game_controller.dart';
import 'package:lichess_mobile/src/model/game/game_preferences.dart';
import 'package:lichess_mobile/src/model/game/playable_game.dart';
Expand Down Expand Up @@ -424,6 +425,8 @@ class _GameBottomBar extends ConsumerWidget {
? ref.watch(chatControllerProvider(id))
: null;

_keepChatPresetsState(ref);

final List<Widget> children = gameStateAsync.when(
data: (gameState) {
final isChatEnabled =
Expand Down Expand Up @@ -861,6 +864,15 @@ class _GameBottomBar extends ConsumerWidget {
onConfirm();
}
}

void _keepChatPresetsState(WidgetRef ref) {
// By listening to the chat presets state we keep it available for the lifetime of the game.
// If we didn't do this, it would be lost each time the chat is closed
ref.listen(
chatPresetsControllerProvider(id),
(previous, next) => {},
);
}
}

class _GameNegotiationDialog extends StatelessWidget {
Expand Down
28 changes: 27 additions & 1 deletion lib/src/view/game/message_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import 'package:flutter_riverpod/flutter_riverpod.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/game/chat_controller.dart';
import 'package:lichess_mobile/src/model/game/chat_presets_controller.dart';
import 'package:lichess_mobile/src/model/game/game_controller.dart';
import 'package:lichess_mobile/src/model/settings/brightness.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/navigation.dart';
import 'package:lichess_mobile/src/styles/lichess_colors.dart';
import 'package:lichess_mobile/src/styles/styles.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/view/game/preset_messages.dart';
import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart';
import 'package:lichess_mobile/src/widgets/buttons.dart';
import 'package:lichess_mobile/src/widgets/platform.dart';
Expand Down Expand Up @@ -99,7 +102,13 @@ class _Body extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final presetsController = chatPresetsControllerProvider(id);
final chatStateAsync = ref.watch(chatControllerProvider(id));
final gameStateAsync = ref.watch(gameControllerProvider(id));
final chatPresetsStateAsync = ref.watch(presetsController);

final myColour = gameStateAsync.value?.game.youAre;
final chatPresetState = chatPresetsStateAsync.value;

return Column(
mainAxisSize: MainAxisSize.max,
Expand All @@ -118,9 +127,14 @@ class _Body extends ConsumerWidget {
itemBuilder: (context, index) {
final message =
chatState.messages[chatState.messages.length - index - 1];

final isMyMessage = message.username != null
? message.username == me?.name
: (message.colour == myColour?.name);

return (message.username == 'lichess')
? _MessageAction(message: message.message)
: (message.username == me?.name)
: isMyMessage
? _MessageBubble(
you: true,
message: message.message,
Expand All @@ -140,6 +154,18 @@ class _Body extends ConsumerWidget {
),
),
),
// Only show presets if the player is participating in the game and the presets state has become available
if (myColour != null && chatPresetState != null)
PresetMessages(
gameId: id,
alreadySaid: chatPresetState.alreadySaid,
presetMessageGroup: chatPresetState.currentPresetMessageGroup,
presetMessages: chatPresetState.presets,
sendChatPreset: (presetMessage) =>
ref.read(presetsController.notifier).sendPreset(presetMessage),
)
else
const SizedBox.shrink(),
_ChatBottomBar(id: id),
],
);
Expand Down
55 changes: 55 additions & 0 deletions lib/src/view/game/preset_messages.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/message_presets.dart';
import 'package:lichess_mobile/src/widgets/buttons.dart';

class PresetMessages extends ConsumerWidget {
final GameFullId gameId;
final List<PresetMessage> alreadySaid;
final PresetMessageGroup? presetMessageGroup;
final Map<PresetMessageGroup, List<PresetMessage>> presetMessages;
final void Function(PresetMessage presetMessage) sendChatPreset;

const PresetMessages({
required this.gameId,
required this.alreadySaid,
required this.presetMessageGroup,
required this.presetMessages,
required this.sendChatPreset,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = presetMessages[presetMessageGroup] ?? [];

if (messages.isEmpty || alreadySaid.length >= 2) {
return const SizedBox.shrink();
}

final notAlreadySaid =
messages.where((message) => !alreadySaid.contains(message));

return Row(
children: notAlreadySaid
.map((preset) => _renderPresetMessageButton(preset, ref))
.toList(),
);
}

Widget _renderPresetMessageButton(PresetMessage preset, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: SecondaryButton(
semanticsLabel: preset.label,
onPressed: () {
sendChatPreset(preset);
},
child: Text(
preset.label,
textAlign: TextAlign.center,
),
),
);
}
}

0 comments on commit 4390ef4

Please sign in to comment.