diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index d9e711e7d5..0e053e4a5d 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -126,11 +126,9 @@ class ChatController extends _$ChatController { final data = event.data as Map; 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), ); } } @@ -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(), ); } diff --git a/lib/src/model/game/chat_presets_controller.dart b/lib/src/model/game/chat_presets_controller.dart new file mode 100644 index 0000000000..2773e17782 --- /dev/null +++ b/lib/src/model/game/chat_presets_controller.dart @@ -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> _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 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 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? previousGame, + AsyncValue 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> presets, + required GameFullId gameId, + required List alreadySaid, + required PresetMessageGroup? currentPresetMessageGroup, + }) = _ChatPresetsState; +} diff --git a/lib/src/model/game/message_presets.dart b/lib/src/model/game/message_presets.dart new file mode 100644 index 0000000000..a5c84f9579 --- /dev/null +++ b/lib/src/model/game/message_presets.dart @@ -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}); diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 1a05b5a6ae..81d397be6d 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -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'; @@ -424,6 +425,8 @@ class _GameBottomBar extends ConsumerWidget { ? ref.watch(chatControllerProvider(id)) : null; + _keepChatPresetsState(ref); + final List children = gameStateAsync.when( data: (gameState) { final isChatEnabled = @@ -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 { diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index 3b91dbb864..ba35d9f191 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -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'; @@ -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, @@ -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, @@ -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), ], ); diff --git a/lib/src/view/game/preset_messages.dart b/lib/src/view/game/preset_messages.dart new file mode 100644 index 0000000000..66ec9292f3 --- /dev/null +++ b/lib/src/view/game/preset_messages.dart @@ -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 alreadySaid; + final PresetMessageGroup? presetMessageGroup; + final Map> 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, + ), + ), + ); + } +}