From c54f3958ebab2e4741eb485fc7c14c4ac79c1247 Mon Sep 17 00:00:00 2001 From: Gordon Martin Date: Thu, 4 Jul 2024 23:07:15 +0100 Subject: [PATCH 1/4] 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 --- lib/src/model/game/chat_controller.dart | 9 +- .../model/game/chat_presets_controller.dart | 107 ++++++++++++++++++ lib/src/model/game/message_presets.dart | 29 +++++ lib/src/view/game/game_body.dart | 12 ++ lib/src/view/game/message_screen.dart | 28 ++++- lib/src/view/game/preset_messages.dart | 55 +++++++++ 6 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 lib/src/model/game/chat_presets_controller.dart create mode 100644 lib/src/model/game/message_presets.dart create mode 100644 lib/src/view/game/preset_messages.dart 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, + ), + ), + ); + } +} From 211ca61b48b9bad6bfa5c2aa2c93f81b0514979d Mon Sep 17 00:00:00 2001 From: Gordon Martin Date: Tue, 9 Jul 2024 21:39:41 +0100 Subject: [PATCH 2/4] wip --- lib/src/model/game/chat_controller.dart | 56 +++++++++ .../model/game/chat_presets_controller.dart | 107 ------------------ lib/src/model/game/message_presets.dart | 10 -- lib/src/view/game/game_body.dart | 12 -- lib/src/view/game/message_screen.dart | 19 ++-- lib/src/view/game/preset_messages.dart | 3 - 6 files changed, 64 insertions(+), 143 deletions(-) delete mode 100644 lib/src/model/game/chat_presets_controller.dart diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 0e053e4a5d..d830c09674 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.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'; import 'package:sqflite/sqflite.dart'; @@ -18,12 +19,30 @@ String _storeKey(GameFullId id) => 'game.$id'; @riverpod class ChatController extends _$ChatController { + 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!'), + ], + }; + StreamSubscription? _subscription; late SocketClient _socketClient; @override Future build(GameFullId id) async { + print("Testaroonie"); + _socketClient = ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); @@ -34,6 +53,15 @@ class ChatController extends _$ChatController { _subscription?.cancel(); }); + final presetMessageGroup = await ref.watch( + gameControllerProvider(id).selectAsync( + (gameState) => PresetMessageGroup.fromGame(gameState.game), + ), + ); + + print("Testaroonie2"); + print(presetMessageGroup); + final messages = await _socketClient.stream .firstWhere((event) => event.topic == 'full') .then( @@ -47,6 +75,11 @@ class ChatController extends _$ChatController { return ChatState( messages: messages ?? IList(), unreadMessages: (messages?.length ?? 0) - readMessagesCount, + chatPresets: ( + presets: _presetMessages, + alreadySaid: [], + currentPresetMessageGroup: null + ), ); } @@ -58,6 +91,22 @@ class ChatController extends _$ChatController { ); } + // Sends a chat preset to the chat and marks it as sent + void sendPreset(PresetMessage message) { + sendMessage(message.value); + + state = state.whenData((s) { + final state = s.copyWith( + chatPresets: ( + alreadySaid: [...s.chatPresets.alreadySaid, message], + currentPresetMessageGroup: s.chatPresets.currentPresetMessageGroup, + presets: s.chatPresets.presets + ), + ); + return state; + }); + } + /// Resets the unread messages count to 0 and saves the number of read messages. Future markMessagesAsRead() async { if (state.hasValue) { @@ -141,9 +190,16 @@ class ChatState with _$ChatState { const factory ChatState({ required IList messages, required int unreadMessages, + required ChatPresets chatPresets, }) = _ChatState; } +typedef ChatPresets = ({ + Map> presets, + List alreadySaid, + PresetMessageGroup? currentPresetMessageGroup +}); + typedef Message = ({String? username, String? colour, String message}); Message _messageFromPick(RequiredPick pick) { diff --git a/lib/src/model/game/chat_presets_controller.dart b/lib/src/model/game/chat_presets_controller.dart deleted file mode 100644 index 2773e17782..0000000000 --- a/lib/src/model/game/chat_presets_controller.dart +++ /dev/null @@ -1,107 +0,0 @@ -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 index a5c84f9579..9ed6cbabaa 100644 --- a/lib/src/model/game/message_presets.dart +++ b/lib/src/model/game/message_presets.dart @@ -5,16 +5,6 @@ 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; diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 81d397be6d..1a05b5a6ae 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -12,7 +12,6 @@ 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'; @@ -425,8 +424,6 @@ class _GameBottomBar extends ConsumerWidget { ? ref.watch(chatControllerProvider(id)) : null; - _keepChatPresetsState(ref); - final List children = gameStateAsync.when( data: (gameState) { final isChatEnabled = @@ -864,15 +861,6 @@ 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 ba35d9f191..dba09dde02 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -4,7 +4,6 @@ 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'; @@ -102,13 +101,11 @@ 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 chatState = chatStateAsync.value; final myColour = gameStateAsync.value?.game.youAre; - final chatPresetState = chatPresetsStateAsync.value; return Column( mainAxisSize: MainAxisSize.max, @@ -155,14 +152,14 @@ 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) + if (myColour != null && chatState != null) PresetMessages( - gameId: id, - alreadySaid: chatPresetState.alreadySaid, - presetMessageGroup: chatPresetState.currentPresetMessageGroup, - presetMessages: chatPresetState.presets, - sendChatPreset: (presetMessage) => - ref.read(presetsController.notifier).sendPreset(presetMessage), + alreadySaid: chatState.chatPresets.alreadySaid, + presetMessageGroup: chatState.chatPresets.currentPresetMessageGroup, + presetMessages: chatState.chatPresets.presets, + sendChatPreset: (presetMessage) => ref + .read(chatControllerProvider(id).notifier) + .sendPreset(presetMessage), ) else const SizedBox.shrink(), diff --git a/lib/src/view/game/preset_messages.dart b/lib/src/view/game/preset_messages.dart index 66ec9292f3..28f91ced52 100644 --- a/lib/src/view/game/preset_messages.dart +++ b/lib/src/view/game/preset_messages.dart @@ -1,18 +1,15 @@ 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, From 04a192dd4124dc8c0943ccfc28399285f72ad273 Mon Sep 17 00:00:00 2001 From: Gordon Martin Date: Wed, 10 Jul 2024 19:31:49 +0100 Subject: [PATCH 3/4] Use ref.listen for now until we can make chat_controller function properly using watch + selectAsync --- lib/src/model/game/chat_controller.dart | 51 +++++++++++++++++-------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index d830c09674..1cece95656 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -41,8 +41,6 @@ class ChatController extends _$ChatController { @override Future build(GameFullId id) async { - print("Testaroonie"); - _socketClient = ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); @@ -53,22 +51,15 @@ class ChatController extends _$ChatController { _subscription?.cancel(); }); - final presetMessageGroup = await ref.watch( - gameControllerProvider(id).selectAsync( - (gameState) => PresetMessageGroup.fromGame(gameState.game), - ), + final messages = await _socketClient.stream.firstWhere((event) { + return event.topic == 'full'; + }).then( + (event) => pick(event.data, 'chat', 'lines') + .asListOrNull(_messageFromPick) + ?.toIList(), ); - print("Testaroonie2"); - print(presetMessageGroup); - - final messages = await _socketClient.stream - .firstWhere((event) => event.topic == 'full') - .then( - (event) => pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(), - ); + ref.listen(gameControllerProvider(id), _handleGameStateChange); final readMessagesCount = await _getReadMessagesCount(); @@ -83,6 +74,34 @@ class ChatController extends _$ChatController { ); } + void _handleGameStateChange( + AsyncValue? previousGame, + AsyncValue currentGame, + ) { + final newGameState = currentGame.value; + + if (newGameState != null) { + final newMessageGroup = PresetMessageGroup.fromGame(newGameState.game); + + final currentMessageGroup = + state.value?.chatPresets.currentPresetMessageGroup; + + if (newMessageGroup != currentMessageGroup) { + state = state.whenData((s) { + final newState = s.copyWith( + chatPresets: ( + currentPresetMessageGroup: newMessageGroup, + presets: _presetMessages, + alreadySaid: [] + ), + ); + + return newState; + }); + } + } + } + /// Sends a message to the chat. void sendMessage(String message) { _socketClient.send( From 8a3146fedea7c7a8e42e921288681bad8e7cef4d Mon Sep 17 00:00:00 2001 From: Gordon Martin Date: Wed, 10 Jul 2024 20:18:49 +0100 Subject: [PATCH 4/4] Use selectAsync --- lib/src/model/game/chat_controller.dart | 63 +++++++++++-------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 1cece95656..06d6d4c6b2 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -51,54 +51,44 @@ class ChatController extends _$ChatController { _subscription?.cancel(); }); - final messages = await _socketClient.stream.firstWhere((event) { - return event.topic == 'full'; - }).then( - (event) => pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(), - ); - - ref.listen(gameControllerProvider(id), _handleGameStateChange); - final readMessagesCount = await _getReadMessagesCount(); + final messages = await _getMessages(); + + final presetMessageGroup = await ref.watch( + gameControllerProvider(id).selectAsync( + (gameState) => PresetMessageGroup.fromGame(gameState.game), + ), + ); return ChatState( + alreadySynced: true, messages: messages ?? IList(), unreadMessages: (messages?.length ?? 0) - readMessagesCount, chatPresets: ( presets: _presetMessages, alreadySaid: [], - currentPresetMessageGroup: null + currentPresetMessageGroup: presetMessageGroup ), ); } - void _handleGameStateChange( - AsyncValue? previousGame, - AsyncValue currentGame, - ) { - final newGameState = currentGame.value; - - if (newGameState != null) { - final newMessageGroup = PresetMessageGroup.fromGame(newGameState.game); - - final currentMessageGroup = - state.value?.chatPresets.currentPresetMessageGroup; - - if (newMessageGroup != currentMessageGroup) { - state = state.whenData((s) { - final newState = s.copyWith( - chatPresets: ( - currentPresetMessageGroup: newMessageGroup, - presets: _presetMessages, - alreadySaid: [] - ), - ); - - return newState; - }); - } + Future?> + _getMessages() { + // Once underlying socket has been opened the 'full' sync message will only be received once + // so when the provider state is rebuilt we should use the existing state + final alreadySynced = state.value?.alreadySynced == true; + final IList? existingMessages = state.value?.messages; + + if (alreadySynced) { + return Future.value(existingMessages); + } else { + return _socketClient.stream.firstWhere((event) { + return event.topic == 'full'; + }).then( + (event) => pick(event.data, 'chat', 'lines') + .asListOrNull(_messageFromPick) + ?.toIList(), + ); } } @@ -207,6 +197,7 @@ class ChatState with _$ChatState { const ChatState._(); const factory ChatState({ + required bool alreadySynced, required IList messages, required int unreadMessages, required ChatPresets chatPresets,