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

In-game chat feature #339

Merged
merged 26 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c66297a
Add the user interface for in-game chat
julien4215 Nov 25, 2023
655ee22
change bubble message color in light and dark mode
julien4215 Oct 14, 2023
17c7c85
add messages from server like "Draw offer sent"
julien4215 Oct 14, 2023
33ef47f
Fix in-game chat screen title
julien4215 Nov 25, 2023
8a289d9
-edit controller to receive and send messages
julien4215 Nov 26, 2023
92276ed
Improvements of chat bottom bar
julien4215 Oct 20, 2023
c2d6c68
-factoring code
julien4215 Nov 26, 2023
eb61bea
small improvements/fixs
julien4215 Nov 26, 2023
8d3b5f3
rename bottom bar class for message screen
julien4215 Nov 26, 2023
d074edf
refactor controller
julien4215 Dec 1, 2023
3a63d7d
fix test
julien4215 Dec 5, 2023
6b58cea
make chat controller more generic
julien4215 Dec 6, 2023
f7afd54
make message screen more generic
julien4215 Dec 6, 2023
d747d17
use AdaptiveTextField instead of TexFfield
julien4215 Dec 7, 2023
2f8ced5
use user full name widget
julien4215 Dec 7, 2023
b9f2452
replace ref.watch by ref.read outside build method
julien4215 Dec 7, 2023
9c32f31
Chat style improvements
veloce Dec 11, 2023
bd1902b
remove useless icon due to fullScreenDialog being true
julien4215 Dec 11, 2023
c6e5d05
Chat tweaks
veloce Dec 12, 2023
c913b69
Fix chat ListView padding on iOS
veloce Dec 12, 2023
87ea390
Dismiss the keyboard on tap
veloce Dec 12, 2023
8b6b666
More chat fixes
veloce Dec 12, 2023
6a66311
Restore maxLines to 4 on chat input
veloce Dec 12, 2023
336c02a
Tweak chat bubble and color
veloce Dec 12, 2023
f715d3e
Add little more contrast to android chat bubbles
veloce Dec 12, 2023
b091be8
Bind chat controller state to a context id
veloce Dec 13, 2023
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
87 changes: 87 additions & 0 deletions lib/src/model/game/chat_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'dart:async';

import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/auth_socket.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'chat_controller.freezed.dart';
part 'chat_controller.g.dart';

@riverpod
class ChatController extends _$ChatController {
StreamSubscription<SocketEvent>? _socketSubscription;

AuthSocket get _socket => ref.read(authSocketProvider);

@override
ChatState build(ID chatContext) {
_socketSubscription = _socket.stream.listen(_handleSocketTopic);

ref.onDispose(() {
_socketSubscription?.cancel();
});

return ChatState(
messages: IList(),
unreadMessages: 0,
);
}

void setMessages(IList<Message> messages) {
state = ChatState(
messages: messages,
unreadMessages: 0,
);
}

void onUserMessage(String message) {
_socket.send(
'talk',
message,
);
}

void resetUnreadMessages() {
state = state.copyWith(unreadMessages: 0);
}

void _handleSocketTopic(SocketEvent event) {
switch (event.topic) {
// Called when a message is received
case 'message':
final data = event.data as Map<String, dynamic>;
final message = data["t"] as String;
final username = data["u"] as String;
state = state.copyWith(
messages: state.messages.add(
Message(
message: message,
username: username,
),
),
unreadMessages: state.unreadMessages + 1,
);
}
}
}

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

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

@immutable
class Message {
final String username;
final String message;

const Message({required this.username, required this.message});
}
30 changes: 20 additions & 10 deletions lib/src/model/game/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/model/common/speed.dart';
import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart';
import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart';
import 'package:lichess_mobile/src/model/game/chat_controller.dart';
import 'package:lichess_mobile/src/model/game/game.dart';
import 'package:lichess_mobile/src/model/game/game_socket_events.dart';
import 'package:lichess_mobile/src/model/game/game_status.dart';
Expand Down Expand Up @@ -59,6 +60,7 @@ class GameController extends _$GameController {
@override
Future<GameState> build(GameFullId gameFullId) {
final socket = ref.watch(authSocketProvider);
final chatNotifier = ref.watch(chatControllerProvider(gameFullId).notifier);
final (stream, _) = socket.connect(Uri(path: '/play/$gameFullId/v6'));
_socketEventVersion = null;
_socketSubscription?.cancel();
Expand All @@ -70,18 +72,22 @@ class GameController extends _$GameController {
_transientMoveTimer?.cancel();
});

return stream.firstWhere((e) => e.topic == 'full').then((event) {
final fullEvent =
GameFullEvent.fromJson(event.data as Map<String, dynamic>);
return stream.firstWhere((e) => e.topic == 'full').then(
(event) {
final fullEvent =
GameFullEvent.fromJson(event.data as Map<String, dynamic>);

_socketEventVersion = fullEvent.socketEventVersion;
_socketEventVersion = fullEvent.socketEventVersion;

return GameState(
game: fullEvent.game,
stepCursor: fullEvent.game.steps.length - 1,
stopClockWaitingForServerAck: false,
);
});
chatNotifier.setMessages(fullEvent.game.messages);

return GameState(
game: fullEvent.game,
stepCursor: fullEvent.game.steps.length - 1,
stopClockWaitingForServerAck: false,
);
},
);
}

void onUserMove(Move move, {bool? isDrop, bool? isPremove}) {
Expand Down Expand Up @@ -431,6 +437,10 @@ class GameController extends _$GameController {
_socketEventVersion = fullEvent.socketEventVersion;
_lastMoveTime = null;

ref
julien4215 marked this conversation as resolved.
Show resolved Hide resolved
.read(chatControllerProvider(gameFullId).notifier)
.setMessages(fullEvent.game.messages);

state = AsyncValue.data(
GameState(
game: fullEvent.game,
Expand Down
11 changes: 11 additions & 0 deletions lib/src/model/game/playable_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/common/perf.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/material_diff.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/utils/json.dart';
Expand Down Expand Up @@ -38,6 +39,7 @@ class PlayableGame
required Player black,
required bool moretimeable,
required bool takebackable,
required IList<Message> messages,

/// The side that the current player is playing as. This is null if viewing
/// the game as a spectator.
Expand Down Expand Up @@ -141,6 +143,7 @@ PlayableGame _playableGameFromPick(RequiredPick pick) {
);
}
}
final messages = pick('chat', 'lines').asListOrThrow(_messageFromPick);

return PlayableGame(
id: requiredGamePick('id').asGameIdOrThrow(),
Expand Down Expand Up @@ -174,6 +177,7 @@ PlayableGame _playableGameFromPick(RequiredPick pick) {
},
),
rematch: pick('game', 'rematch').asGameIdOrNull(),
messages: messages.toIList(),
);
}

Expand Down Expand Up @@ -248,3 +252,10 @@ CorrespondenceClockData _correspondenceClockDataFromPick(RequiredPick pick) {
black: pick('black').asDurationFromSecondsOrThrow(),
);
}

Message _messageFromPick(RequiredPick pick) {
return Message(
message: pick('t').asStringOrThrow(),
username: pick('u').asStringOrThrow(),
);
}
64 changes: 61 additions & 3 deletions lib/src/view/game/game_body.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;

import 'package:chessground/chessground.dart' as cg;
import 'package:collection/collection.dart';
Expand All @@ -10,6 +11,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
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/game_controller.dart';
import 'package:lichess_mobile/src/model/game/game_repository_providers.dart';
import 'package:lichess_mobile/src/model/lobby/game_seek.dart';
Expand All @@ -21,10 +23,12 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/utils/navigation.dart';
import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart';
import 'package:lichess_mobile/src/view/game/correspondence_clock_widget.dart';
import 'package:lichess_mobile/src/view/game/message_screen.dart';
import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
import 'package:lichess_mobile/src/widgets/board_table.dart';
import 'package:lichess_mobile/src/widgets/buttons.dart';
import 'package:lichess_mobile/src/widgets/countdown_clock.dart';
import 'package:lichess_mobile/src/widgets/user_full_name.dart';
import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart';

import 'game_common_widgets.dart';
Expand Down Expand Up @@ -342,6 +346,7 @@ class _GameBottomBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final ongoingGames = ref.watch(ongoingGamesProvider);
final chatState = ref.watch(chatControllerProvider(id));

return Container(
padding: Styles.horizontalBodyPadding,
Expand Down Expand Up @@ -523,9 +528,62 @@ class _GameBottomBar extends ConsumerWidget {
orElse: () => null,
),
),
// TODO replace this space with chat button
const SizedBox(
width: 44.0,
Stack(
children: [
BottomBarButton(
label: context.l10n.chat,
shortLabel: context.l10n.chat,
onTap: () {
pushPlatformRoute(
context,
fullscreenDialog: true,
builder: (BuildContext context) {
return MessageScreen(
title: UserFullNameWidget(
user: gameState.game.opponent?.user,
),
me: gameState.game.me?.user,
chatContext: id,
);
},
);
},
icon: defaultTargetPlatform == TargetPlatform.iOS
? CupertinoIcons.chat_bubble
: Icons.chat_bubble_outline,
),
if (chatState.unreadMessages > 0)
Positioned(
top: 2.0,
right: 2.0,
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.brightness_1,
size: 20.0,
color: defaultTargetPlatform == TargetPlatform.iOS
? CupertinoColors.activeBlue.resolveFrom(context)
: Theme.of(context).colorScheme.primary,
),
FittedBox(
fit: BoxFit.contain,
child: Text(
math.min(9, chatState.unreadMessages).toString(),
style: TextStyle(
color: defaultTargetPlatform == TargetPlatform.iOS
? Colors.white
: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
],
),
)
else
const SizedBox.shrink(),
],
),
RepeatButton(
onLongPress:
Expand Down
2 changes: 2 additions & 0 deletions lib/src/view/game/lobby_game_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class _GameScreenState extends ConsumerState<LobbyGameScreen>

Widget _androidBuilder(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: GameAppBar(seek: widget.seek),
body: _LoadLobbyGame(
seek: widget.seek,
Expand All @@ -76,6 +77,7 @@ class _GameScreenState extends ConsumerState<LobbyGameScreen>

Widget _iosBuilder(BuildContext context) {
return CupertinoPageScaffold(
resizeToAvoidBottomInset: false,
navigationBar: GameCupertinoNavBar(seek: widget.seek),
child: _LoadLobbyGame(
seek: widget.seek,
Expand Down
Loading