From 77fb4bcf4e5311a837e2e7e33d032cef0099f6b2 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:40:41 -0400 Subject: [PATCH] Revert "base timer off of game state event" --- lib/pages/chat/chat.dart | 44 +++-- lib/pages/chat/chat_event_list.dart | 9 +- lib/pages/chat/chat_view.dart | 3 +- lib/pangea/constants/game_constants.dart | 3 - lib/pangea/constants/model_keys.dart | 4 - lib/pangea/constants/pangea_event_types.dart | 2 - lib/pangea/models/game_state_model.dart | 48 ------ .../pages/games/story_game/round_model.dart | 123 +++++-------- lib/pangea/widgets/chat/round_timer.dart | 161 ++++++++---------- 9 files changed, 131 insertions(+), 266 deletions(-) delete mode 100644 lib/pangea/constants/game_constants.dart delete mode 100644 lib/pangea/models/game_state_model.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 79824d662..6e6a7487f 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -15,12 +15,10 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; -import 'package:fluffychat/pangea/models/game_state_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/pages/games/story_game/round_model.dart'; @@ -28,6 +26,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/report_message.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/widgets/chat/round_timer.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -116,9 +115,17 @@ class ChatController extends State // #Pangea final PangeaController pangeaController = MatrixState.pangeaController; late Choreographer choreographer = Choreographer(pangeaController, this); + final GlobalKey roundTimerStateKey = + GlobalKey(); + RoundTimer? timer; - /// Model of the current story game round - GameRoundModel? currentRound; + final List gameRounds = []; + + List get completedRoundEventIds => gameRounds + .where((round) => round.isCompleted) + .map((round) => round.userMessageIDs) + .expand((x) => x) + .toList(); // Pangea# Room get room => sendingClient.getRoomById(roomId) ?? widget.room; @@ -301,22 +308,12 @@ class ChatController extends State } // #Pangea - /// Recursive function that sets the current round, waits for it to - /// finish, sets it, etc. until the chat view is no longer mounted. - void setRound() { - currentRound?.dispose(); - currentRound = GameRoundModel(room: room); - room.client.onRoomState.stream.firstWhere((update) { - if (update.roomId != roomId) return false; - if (update.state is! Event) return false; - if ((update.state as Event).type != PangeaEventTypes.storyGame) { - return false; - } - - final game = GameModel.fromJson((update.state as Event).content); - return game.previousRoundEndTime != null; - }).then((_) { - if (mounted) setRound(); + void addRound() { + debugPrint("ADDING A ROUND. Rounds so far: ${gameRounds.length}"); + final newRound = GameRoundModel(controller: this, timer: timer!); + gameRounds.add(newRound); + newRound.roundCompleter.future.then((_) { + if (mounted) addRound(); }); } // Pangea# @@ -336,7 +333,8 @@ class ChatController extends State sendingClient = Matrix.of(context).client; WidgetsBinding.instance.addObserver(this); // #Pangea - setRound(); + timer = RoundTimer(key: roundTimerStateKey); + addRound(); if (!mounted) return; Future.delayed(const Duration(seconds: 1), () async { if (!mounted) return; @@ -423,7 +421,8 @@ class ChatController extends State List get visibleEvents => timeline?.events .where( - (x) => x.isVisibleInGui, + (x) => + x.isVisibleInGui && !completedRoundEventIds.contains(x.eventId), ) .toList() ?? []; @@ -561,7 +560,6 @@ class ChatController extends State //#Pangea choreographer.stateListener.close(); choreographer.dispose(); - currentRound?.dispose(); //Pangea# super.dispose(); } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 1e216094a..7b9003379 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -33,13 +32,7 @@ class ChatEventList extends StatelessWidget { event.isVisibleInGui // #Pangea && - // In story game, hide messages sent by non-bot users in previous round - (event.type != EventTypes.Message || - event.senderId == BotName.byEnvironment || - controller.currentRound?.previousRoundEnd == null || - event.originServerTs.isAfter( - controller.currentRound!.previousRoundEnd!, - )) + !controller.completedRoundEventIds.contains(event.eventId) // Pangea# , ) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 876db6e68..ce45043eb 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -13,7 +13,6 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart'; -import 'package:fluffychat/pangea/widgets/chat/round_timer.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; @@ -120,7 +119,7 @@ class ChatView extends StatelessWidget { // #Pangea } else { return [ - RoundTimer(controller: controller), + controller.timer ?? const SizedBox(), const SizedBox( width: 10, ), diff --git a/lib/pangea/constants/game_constants.dart b/lib/pangea/constants/game_constants.dart deleted file mode 100644 index 6b0b22fbb..000000000 --- a/lib/pangea/constants/game_constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class GameConstants { - static const int timerMaxSeconds = 120; -} diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index e427cf098..b42061446 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -119,8 +119,4 @@ class ModelKey { static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; - - static const String gameState = "game_state"; - static const String currentRoundStartTime = "start_time"; - static const String previousRoundEndTime = "message_visible_from"; } diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index ab5d655a7..9ca975dc0 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -35,6 +35,4 @@ class PangeaEventTypes { /// A record of completion of an activity. There /// can be one per user per activity. static const activityRecord = "pangea.activity_completion"; - - static const storyGame = "p.game.story"; } diff --git a/lib/pangea/models/game_state_model.dart b/lib/pangea/models/game_state_model.dart deleted file mode 100644 index 12e1bb695..000000000 --- a/lib/pangea/models/game_state_model.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:developer'; - -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/foundation.dart'; -import 'package:matrix/matrix_api_lite/generated/model.dart'; - -class GameModel { - DateTime? currentRoundStartTime; - DateTime? previousRoundEndTime; - - GameModel({ - this.currentRoundStartTime, - this.previousRoundEndTime, - }); - - factory GameModel.fromJson(json) { - return GameModel( - currentRoundStartTime: json[ModelKey.currentRoundStartTime] != null - ? DateTime.parse(json[ModelKey.currentRoundStartTime]) - : null, - previousRoundEndTime: json[ModelKey.previousRoundEndTime] != null - ? DateTime.parse(json[ModelKey.previousRoundEndTime]) - : null, - ); - } - - Map toJson() { - final data = {}; - try { - data[ModelKey.currentRoundStartTime] = - currentRoundStartTime?.toIso8601String(); - data[ModelKey.previousRoundEndTime] = - previousRoundEndTime?.toIso8601String(); - return data; - } catch (e, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, s: s); - return data; - } - } - - StateEvent get toStateEvent => StateEvent( - content: toJson(), - type: PangeaEventTypes.storyGame, - ); -} diff --git a/lib/pangea/pages/games/story_game/round_model.dart b/lib/pangea/pages/games/story_game/round_model.dart index 3b43b86b5..c192c36a6 100644 --- a/lib/pangea/pages/games/story_game/round_model.dart +++ b/lib/pangea/pages/games/story_game/round_model.dart @@ -1,84 +1,62 @@ import 'dart:async'; -import 'package:fluffychat/pangea/constants/game_constants.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; -import 'package:fluffychat/pangea/models/game_state_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/widgets/chat/round_timer.dart'; +import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -/// A model of a game round. Manages the round's state and duration. +enum RoundState { notStarted, inProgress, completed } + class GameRoundModel { - final Duration roundDuration = const Duration( - seconds: GameConstants.timerMaxSeconds, - ); + static const int timerMaxSeconds = 180; + final String adminName = BotName.byEnvironment; - final Room room; - - // All the below state variables are used for sending and managing - // round start and end times. Once the bot starts doing that, they should be removed. + final ChatController controller; + final Completer roundCompleter = Completer(); late DateTime createdAt; - Timer? timer; + RoundTimer timer; + DateTime? startTime; + DateTime? endTime; + RoundState state = RoundState.notStarted; StreamSubscription? syncSubscription; final List userMessageIDs = []; final List botMessageIDs = []; GameRoundModel({ - required this.room, + required this.controller, + required this.timer, }) { createdAt = DateTime.now(); - - // if, on creation, the current round is already ongoing, - // start the timer (or reset it if the round went over) - if (currentRoundStart != null) { - final currentRoundDuration = DateTime.now().difference( - currentRoundStart!, - ); - final roundFinished = currentRoundDuration > roundDuration; - - if (roundFinished) { - endRound(); - } - } - - // listen to syncs for new bot messages to start and stop rounds - syncSubscription ??= room.client.onSync.stream.listen(_handleSync); + syncSubscription ??= client.onSync.stream.listen(_handleSync); } - GameModel get gameState => GameModel.fromJson( - room.getState(PangeaEventTypes.storyGame)?.content ?? {}, - ); - - DateTime? get currentRoundStart => gameState.currentRoundStartTime; - DateTime? get previousRoundEnd => gameState.previousRoundEndTime; - void _handleSync(SyncUpdate update) { final newMessages = update - .messages(room) + .messages(controller.room) .where((msg) => msg.originServerTs.isAfter(createdAt)) .toList(); - final botMessages = newMessages - .where((msg) => msg.senderId == BotName.byEnvironment) - .toList(); - final userMessages = newMessages - .where((msg) => msg.senderId != BotName.byEnvironment) - .toList(); + final botMessages = + newMessages.where((msg) => msg.senderId == adminName).toList(); + final userMessages = + newMessages.where((msg) => msg.senderId != adminName).toList(); final hasNewBotMessage = botMessages.any( (msg) => !botMessageIDs.contains(msg.eventId), ); if (hasNewBotMessage) { - if (currentRoundStart == null) { + if (state == RoundState.notStarted) { startRound(); - } else { + } else if (state == RoundState.inProgress) { endRound(); return; } } - if (currentRoundStart != null) { + if (state == RoundState.inProgress) { for (final message in botMessages) { if (!botMessageIDs.contains(message.eventId)) { botMessageIDs.add(message.eventId); @@ -93,53 +71,32 @@ class GameRoundModel { } } - /// Set the start and end times of the current and previous rounds. - Future setRoundTimes({ - DateTime? currentRoundStart, - DateTime? previousRoundEnd, - }) async { - final game = GameModel.fromJson( - room.getState(PangeaEventTypes.storyGame)?.content ?? {}, - ); + Client get client => controller.pangeaController.matrixState.client; - game.currentRoundStartTime = currentRoundStart; - game.previousRoundEndTime = previousRoundEnd; + bool get isCompleted => roundCompleter.isCompleted; - await room.client.setRoomStateWithKey( - room.id, - PangeaEventTypes.storyGame, - '', - game.toJson(), - ); - } - - /// Start a new round. void startRound() { - setRoundTimes( - currentRoundStart: DateTime.now(), - previousRoundEnd: null, - ).then((_) => timer = Timer(roundDuration, endRound)); + debugPrint("starting round"); + state = RoundState.inProgress; + startTime = DateTime.now(); + controller.roundTimerStateKey.currentState?.resetTimer( + roundLength: timerMaxSeconds, + ); + controller.roundTimerStateKey.currentState?.startTimer(); } - /// End and cleanup after the current round. void endRound() { - syncSubscription?.cancel(); - syncSubscription = null; - - timer?.cancel(); - timer = null; - - setRoundTimes( - currentRoundStart: null, - previousRoundEnd: DateTime.now(), + debugPrint( + "ending round, user message IDs: $userMessageIDs, bot message IDs: $botMessageIDs", ); + endTime = DateTime.now(); + state = RoundState.completed; + controller.roundTimerStateKey.currentState?.resetTimer(); + syncSubscription?.cancel(); + roundCompleter.complete(); } void dispose() { syncSubscription?.cancel(); - syncSubscription = null; - - timer?.cancel(); - timer = null; } } diff --git a/lib/pangea/widgets/chat/round_timer.dart b/lib/pangea/widgets/chat/round_timer.dart index 5153fee12..83641983d 100644 --- a/lib/pangea/widgets/chat/round_timer.dart +++ b/lib/pangea/widgets/chat/round_timer.dart @@ -1,20 +1,17 @@ import 'dart:async'; -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/constants/game_constants.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/models/game_state_model.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; /// Create a timer that counts down to the given time /// Default duration is 180 seconds class RoundTimer extends StatefulWidget { - final ChatController controller; + final int timerMaxSeconds; + final Duration roundDuration; + const RoundTimer({ super.key, - required this.controller, + this.timerMaxSeconds = 180, + this.roundDuration = const Duration(seconds: 1), }); @override @@ -23,112 +20,90 @@ class RoundTimer extends StatefulWidget { class RoundTimerState extends State { int currentSeconds = 0; - Timer? timer; - StreamSubscription? stateSubscription; + Timer? _timer; + bool isTiming = false; + Duration? duration; + int timerMaxSeconds = 180; + + void resetTimer({Duration? roundDuration, int? roundLength}) { + if (_timer != null) { + _timer!.cancel(); + isTiming = false; + } + if (roundDuration != null) { + duration = roundDuration; + } + if (roundLength != null) { + timerMaxSeconds = roundLength; + } + setState(() { + currentSeconds = 0; + }); + } + + int get remainingTime => timerMaxSeconds - currentSeconds; + + String get timerText => + '${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}'; + + startTimer() { + _timer = Timer.periodic(duration ?? widget.roundDuration, (timer) { + setState(() { + currentSeconds++; + if (currentSeconds >= timerMaxSeconds) timer.cancel(); + }); + }); + setState(() { + isTiming = true; + }); + } + + stopTimer() { + if (_timer != null) { + _timer!.cancel(); + } + setState(() { + isTiming = false; + }); + } @override void initState() { + duration = widget.roundDuration; + timerMaxSeconds = widget.timerMaxSeconds; super.initState(); - - final roundStartTime = widget.controller.currentRound?.currentRoundStart; - if (roundStartTime != null) { - final roundDuration = DateTime.now().difference(roundStartTime).inSeconds; - if (roundDuration > GameConstants.timerMaxSeconds) return; - - currentSeconds = roundDuration; - timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { - currentSeconds++; - if (currentSeconds >= GameConstants.timerMaxSeconds) { - t.cancel(); - } - setState(() {}); - }); - } - - stateSubscription = Matrix.of(context) - .client - .onRoomState - .stream - .where(isRoundUpdate) - .listen(onRoundUpdate); - } - - bool isRoundUpdate(update) { - return update.roomId == widget.controller.room.id && - update.state is Event && - (update.state as Event).type == PangeaEventTypes.storyGame; - } - - void onRoundUpdate(update) { - final GameModel gameState = GameModel.fromJson( - (update.state as Event).content, - ); - final startTime = gameState.currentRoundStartTime; - final endTime = gameState.previousRoundEndTime; - - if (startTime == null && endTime == null) return; - timer?.cancel(); - timer = null; - - // if this update is the start of a round - if (startTime != null) { - timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { - currentSeconds++; - if (currentSeconds >= GameConstants.timerMaxSeconds) { - t.cancel(); - } - setState(() {}); - }); - return; - } - - // if this update is the end of a round - currentSeconds = 0; - setState(() {}); } @override void dispose() { + if (_timer != null) { + _timer!.cancel(); + } super.dispose(); - - stateSubscription?.cancel(); - stateSubscription = null; - - timer?.cancel(); - timer = null; } - int get remainingTime => GameConstants.timerMaxSeconds - currentSeconds; - - String get timerText => - '${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}'; - @override Widget build(BuildContext context) { return Material( color: const Color.fromARGB(255, 126, 22, 14), child: Padding( - padding: const EdgeInsets.all(5), + padding: const EdgeInsets.all( + 5, + ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(timerText), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // IconButton( - // onPressed: widget.currentRound.timer == null - // ? widget.currentRound.startRound - // : null, - // icon: Icon( - // widget.currentRound.timer != null - // ? Icons.pause_circle - // : Icons.play_circle, - // ), - // ), - ], - ), + // Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // IconButton( + // onPressed: isTiming ? stopTimeout : startTimeout, + // icon: Icon(isTiming ? Icons.pause_circle : Icons.play_circle), + // ), + // ], + // ), ], ), ),