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 all commits
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
91 changes: 78 additions & 13 deletions lib/src/model/game/chat_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,6 +19,22 @@ String _storeKey(GameFullId id) => 'game.$id';

@riverpod
class ChatController extends _$ChatController {
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!'),
],
};

StreamSubscription<SocketEvent>? _subscription;

late SocketClient _socketClient;
Expand All @@ -34,22 +51,47 @@ class ChatController extends _$ChatController {
_subscription?.cancel();
});

final messages = await _socketClient.stream
.firstWhere((event) => event.topic == 'full')
.then(
(event) => pick(event.data, 'chat', 'lines')
.asListOrNull(_messageFromPick)
?.toIList(),
);

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

Future<IList<({String? colour, String message, String? username})>?>
_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<Message>? 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(),
);
}
}

/// Sends a message to the chat.
void sendMessage(String message) {
_socketClient.send(
Expand All @@ -58,6 +100,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<void> markMessagesAsRead() async {
if (state.hasValue) {
Expand Down Expand Up @@ -126,11 +184,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 @@ -141,16 +197,25 @@ class ChatState with _$ChatState {
const ChatState._();

const factory ChatState({
required bool alreadySynced,
required IList<Message> messages,
required int unreadMessages,
required ChatPresets chatPresets,
}) = _ChatState;
}

typedef Message = ({String? username, String message});
typedef ChatPresets = ({
Map<PresetMessageGroup, List<PresetMessage>> presets,
List<PresetMessage> alreadySaid,
PresetMessageGroup? currentPresetMessageGroup
});

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(),
);
}
19 changes: 19 additions & 0 deletions lib/src/model/game/message_presets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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? 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});
25 changes: 24 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,14 @@ 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/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 @@ -100,6 +102,10 @@ class _Body extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final chatStateAsync = ref.watch(chatControllerProvider(id));
final gameStateAsync = ref.watch(gameControllerProvider(id));

final chatState = chatStateAsync.value;
final myColour = gameStateAsync.value?.game.youAre;

return Column(
mainAxisSize: MainAxisSize.max,
Expand All @@ -118,9 +124,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 +151,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 && chatState != null)
PresetMessages(
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(),
_ChatBottomBar(id: id),
],
);
Expand Down
52 changes: 52 additions & 0 deletions lib/src/view/game/preset_messages.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/game/message_presets.dart';
import 'package:lichess_mobile/src/widgets/buttons.dart';

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

const PresetMessages({
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,
),
),
);
}
}