Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce preset messages into the app #825

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok now I understand why the state is invalidated.

I think the easiest way to fix this is to remove the chatPresetController and put its logic in the chatController. This will simplify things hopefully as this logic belongs to the chat controller, and you won't have to worry about state invalidation anymore.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to understand why you need a private local var here.


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

ref.listen(gameControllerProvider(_gameId), _handleGameStateChange);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I don't think ref.listen is the best way to handle this.

Since we want to act upon the game state, ref.watch(gameControllerProvider(id)) feels more like what we want.
We don't want to rebuild the chat state each time the game state changes, so you can also use selectAsync to filter rebuild by returning only PresetMessageGroup when they change.


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) {
Happy0 marked this conversation as resolved.
Show resolved Hide resolved
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);
Happy0 marked this conversation as resolved.
Show resolved Hide resolved

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,
),
),
);
}
}