Merge branch 'main' into learning-analytics-summary

This commit is contained in:
ggurdin 2024-08-23 10:31:00 -04:00
commit f8020279c6
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
25 changed files with 560 additions and 214 deletions

View file

@ -3,13 +3,23 @@ name: Main Deploy Workflow
on:
push:
branches:
- main
- development
workflow_dispatch:
env:
WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
jobs:
switch-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout main branch
uses: actions/checkout@v3
- name: Checkout different branch
run: git checkout development
build_web:
runs-on: ubuntu-latest
environment: staging

View file

@ -3065,7 +3065,7 @@
"type": "text",
"placeholders": {}
},
"toggleToolSettingsDescription": "Here you can toggle your individual language tool settings. For chats within a space, the space settings will take precedence and may override these settings.",
"toggleToolSettingsDescription": "Here you can toggle your individual language tool settings.",
"connectedToStaging": "You are connected to the staging server.",
"@connectedToStaging": {
"type": "text",

View file

@ -15,12 +15,15 @@ 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';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/utils/report_message.dart';
@ -112,9 +115,12 @@ class ChatController extends State<ChatPageWithRoom>
with WidgetsBindingObserver {
// #Pangea
final PangeaController pangeaController = MatrixState.pangeaController;
late Choreographer choreographer = Choreographer(pangeaController, this);
/// Model of the current story game round
GameRoundModel? currentRound;
// Pangea#
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
late Client sendingClient;
@ -294,6 +300,27 @@ class ChatController extends State<ChatPageWithRoom>
}
}
// #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();
});
}
// Pangea#
@override
void initState() {
scrollController.addListener(_updateScrollController);
@ -309,6 +336,7 @@ class ChatController extends State<ChatPageWithRoom>
sendingClient = Matrix.of(context).client;
WidgetsBinding.instance.addObserver(this);
// #Pangea
setRound();
if (!mounted) return;
Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return;
@ -533,6 +561,7 @@ class ChatController extends State<ChatPageWithRoom>
//#Pangea
choreographer.stateListener.close();
choreographer.dispose();
currentRound?.dispose();
//Pangea#
super.dispose();
}

View file

@ -6,6 +6,7 @@ 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';
@ -27,7 +28,21 @@ class ChatEventList extends StatelessWidget {
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
final events = controller.timeline!.events
.where((event) => event.isVisibleInGui)
.where(
(event) =>
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!,
))
// Pangea#
,
)
.toList();
final animateInEventIndex = controller.animateInEventIndex;

View file

@ -14,6 +14,7 @@ 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/animations/gain_points.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,6 +121,10 @@ class ChatView extends StatelessWidget {
// #Pangea
} else {
return [
RoundTimer(controller: controller),
const SizedBox(
width: 10,
),
ChatSettingsPopupMenu(
controller.room,
(!controller.room.isDirectChat && !controller.room.isArchived),

View file

@ -1,14 +1,12 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
extension EventInfoDialogExtension on Event {
void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet(
@ -49,15 +47,16 @@ class EventInfoDialog extends StatelessWidget {
children: [
ListTile(
leading: Avatar(
mxContent: event.senderFromMemoryOrFallback.avatarUrl,
name: event.senderFromMemoryOrFallback.calcDisplayname(),
// mxContent: event.senderFromMemoryOrFallback.avatarUrl,
// name: event.senderFromMemoryOrFallback.calcDisplayname(),
name: "?",
client: event.room.client,
presenceUserId: event.senderId,
// presenceUserId: event.senderId,
),
title: Text(L10n.of(context)!.sender),
subtitle: Text(
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
),
// subtitle: Text(
// '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
// ),
),
ListTile(
title: Text(L10n.of(context)!.time),

View file

@ -263,9 +263,10 @@ class Message extends StatelessWidget {
final user = snapshot.data ??
event.senderFromMemoryOrFallback;
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
// mxContent: user.avatarUrl,
// name: user.calcDisplayname(),
// presenceUserId: user.stateKey,
name: "?",
presenceBackgroundColor:
avatarPresenceBackgroundColor,
onTap: () => onAvatarTab(event),
@ -288,10 +289,11 @@ class Message extends StatelessWidget {
: FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname = snapshot.data
?.calcDisplayname() ??
event.senderFromMemoryOrFallback
.calcDisplayname();
// final displayname = snapshot.data
// ?.calcDisplayname() ??
// event.senderFromMemoryOrFallback
// .calcDisplayname();
const displayname = "?";
return Text(
displayname,
style: TextStyle(

View file

@ -96,12 +96,16 @@ class MessageContent extends StatelessWidget {
ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
mxContent: sender.avatarUrl,
name: sender.calcDisplayname(),
presenceUserId: sender.stateKey,
// mxContent: sender.avatarUrl,
// name: sender.calcDisplayname(),
// presenceUserId: sender.stateKey,
name: "?",
client: event.room.client,
),
title: Text(sender.calcDisplayname()),
title: const Text(
// sender.calcDisplayname(),
"?",
),
subtitle: Text(event.originServerTs.localizedTime(context)),
trailing: const Icon(Icons.lock_outlined),
),
@ -265,9 +269,10 @@ class MessageContent extends StatelessWidget {
builder: (context, snapshot) {
final reason =
event.redactedBecause?.content.tryGet<String>('reason');
final redactedBy = snapshot.data?.calcDisplayname() ??
event.redactedBecause?.senderId.localpart ??
L10n.of(context)!.user;
// final redactedBy = snapshot.data?.calcDisplayname() ??
// event.redactedBecause?.senderId.localpart ??
// L10n.of(context)!.user;
const redactedBy = "?";
return _ButtonContent(
label: reason == null
? L10n.of(context)!.redactedBy(redactedBy)
@ -385,8 +390,9 @@ class MessageContent extends StatelessWidget {
builder: (context, snapshot) {
return _ButtonContent(
label: L10n.of(context)!.userSentUnknownEvent(
snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname(),
// snapshot.data?.calcDisplayname() ??
// event.senderFromMemoryOrFallback.calcDisplayname(),
"?",
event.type,
),
icon: '',

View file

@ -59,7 +59,8 @@ class ReplyContent extends StatelessWidget {
future: displayEvent.fetchSenderUser(),
builder: (context, snapshot) {
return Text(
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
// '${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
'?:',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(

View file

@ -37,9 +37,10 @@ class SeenByRow extends StatelessWidget {
? seenByUsers.sublist(0, maxAvatars)
: seenByUsers)
.map(
(user) => Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
(user) => const Avatar(
// mxContent: user.avatarUrl,
// name: user.calcDisplayname(),
name: "?",
size: 16,
),
),

View file

@ -1,12 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class TypingIndicators extends StatelessWidget {
final ChatController controller;
@ -56,10 +55,11 @@ class TypingIndicators extends StatelessWidget {
child: Stack(
children: [
if (typingUsers.isNotEmpty)
Avatar(
const Avatar(
size: avatarSize,
mxContent: typingUsers.first.avatarUrl,
name: typingUsers.first.calcDisplayname(),
// mxContent: typingUsers.first.avatarUrl,
// name: typingUsers.first.calcDisplayname(),
name: "?",
),
if (typingUsers.length == 2)
Padding(
@ -69,9 +69,10 @@ class TypingIndicators extends StatelessWidget {
mxContent: typingUsers.length == 2
? typingUsers.last.avatarUrl
: null,
name: typingUsers.length == 2
? typingUsers.last.calcDisplayname()
: '+${typingUsers.length - 1}',
// name: typingUsers.length == 2
// ? typingUsers.last.calcDisplayname()
// : '+${typingUsers.length - 1}',
name: "?",
),
),
],

View file

@ -39,9 +39,10 @@ class ParticipantListItem extends StatelessWidget {
),
title: Row(
children: <Widget>[
Expanded(
const Expanded(
child: Text(
user.calcDisplayname(),
// user.calcDisplayname(),
"?",
overflow: TextOverflow.ellipsis,
),
),
@ -88,8 +89,9 @@ class ParticipantListItem extends StatelessWidget {
subtitle: Text(user.id),
leading: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
// name: user.calcDisplayname(),
// presenceUserId: user.stateKey,
name: "?",
),
),
);

View file

@ -1,11 +1,9 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/get_chat_list_item_subtitle.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/room_status_extension.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
@ -240,51 +238,52 @@ class ChatListItem extends StatelessWidget {
softWrap: false,
)
// #Pangea
: FutureBuilder<String>(
future: room.lastEvent != null
? GetChatListItemSubtitle().getSubtitle(
L10n.of(context)!,
room.lastEvent,
MatrixState.pangeaController,
)
: Future.value(L10n.of(context)!.emptyChat),
builder: (context, snapshot) {
// Pangea#
return Text(
room.membership == Membership.invite
? isDirectChat
? L10n.of(context)!.invitePrivateChat
: L10n.of(context)!.inviteGroupChat
// #Pangea
: snapshot.data ??
// Pangea#
room.lastEvent
?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: !isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId,
) ??
L10n.of(context)!.emptyChat,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.bold
: null,
color: theme.colorScheme.onSurfaceVariant,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
);
},
),
: const SizedBox(),
// FutureBuilder<String>(
// future: room.lastEvent != null
// ? GetChatListItemSubtitle().getSubtitle(
// L10n.of(context)!,
// room.lastEvent,
// MatrixState.pangeaController,
// )
// : Future.value(L10n.of(context)!.emptyChat),
// builder: (context, snapshot) {
// // Pangea#
// return Text(
// room.membership == Membership.invite
// ? isDirectChat
// ? L10n.of(context)!.invitePrivateChat
// : L10n.of(context)!.inviteGroupChat
// // #Pangea
// : snapshot.data ??
// // Pangea#
// room.lastEvent
// ?.calcLocalizedBodyFallback(
// MatrixLocals(L10n.of(context)!),
// hideReply: true,
// hideEdit: true,
// plaintextBody: true,
// removeMarkdown: true,
// withSenderNamePrefix: !isDirectChat ||
// directChatMatrixId !=
// room.lastEvent?.senderId,
// ) ??
// L10n.of(context)!.emptyChat,
// softWrap: false,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// style: TextStyle(
// fontWeight: unread || room.hasNewMessages
// ? FontWeight.bold
// : null,
// color: theme.colorScheme.onSurfaceVariant,
// decoration: room.lastEvent?.redacted == true
// ? TextDecoration.lineThrough
// : null,
// ),
// );
// },
// ),
),
const SizedBox(width: 8),
// #Pangea

View file

@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
class ChatSearchMessageTab extends StatelessWidget {
final String searchQuery;
@ -98,9 +96,10 @@ class ChatSearchMessageTab extends StatelessWidget {
}
final event = events[i];
final sender = event.senderFromMemoryOrFallback;
final displayname = sender.calcDisplayname(
i18n: MatrixLocals(L10n.of(context)!),
);
// final displayname = sender.calcDisplayname(
// i18n: MatrixLocals(L10n.of(context)!),
// );
const displayname = "?";
return _MessageSearchResultListTile(
sender: sender,
displayname: displayname,

View file

@ -22,9 +22,10 @@ class UserBottomSheetView extends StatelessWidget {
Widget build(BuildContext context) {
final user = controller.widget.user;
final userId = (user?.id ?? controller.widget.profile?.userId)!;
final displayname = (user?.calcDisplayname() ??
controller.widget.profile?.displayName ??
controller.widget.profile?.userId.localpart)!;
// final displayname = (user?.calcDisplayname() ??
// controller.widget.profile?.displayName ??
// controller.widget.profile?.userId.localpart)!;
const displayname = "?";
final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl;
final client = Matrix.of(controller.widget.outerContext).client;
@ -39,7 +40,7 @@ class UserBottomSheetView extends StatelessWidget {
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayname),
const Text(displayname),
PresenceBuilder(
userId: userId,
client: client,
@ -213,7 +214,7 @@ class UserBottomSheetView extends StatelessWidget {
foregroundColor:
Theme.of(context).colorScheme.onSurface,
),
label: Text(
label: const Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,

View file

@ -15,24 +15,21 @@ class ChoreographerHasErrorButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: () {
if (error.type == ChoreoErrorType.unknown) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
"${error.title(context)} ${error.description(context)}",
),
return FloatingActionButton(
onPressed: () {
if (error.type == ChoreoErrorType.unknown) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
"${error.title(context)} ${error.description(context)}",
),
);
}
},
mini: true,
child: Icon(error.icon),
),
),
);
}
},
mini: true,
child: Icon(error.icon),
);
}
}

View file

@ -0,0 +1,3 @@
class GameConstants {
static const int timerMaxSeconds = 120;
}

View file

@ -119,4 +119,8 @@ 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";
}

View file

@ -35,4 +35,6 @@ 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";
}

View file

@ -1,85 +1,20 @@
import 'package:matrix/matrix.dart';
extension MembershipUpdate on SyncUpdate {
bool isMembershipUpdate(String userId) {
return isMembershipUpdateByType(Membership.join, userId) ||
isMembershipUpdateByType(Membership.leave, userId) ||
isMembershipUpdateByType(Membership.invite, userId);
}
bool isMembershipUpdateByType(Membership type, String userId) {
final List<SyncRoomUpdate>? updates = getRoomUpdates(type);
if (updates?.isEmpty ?? true) {
return false;
List<Event> messages(Room chat) {
if (rooms?.join == null ||
!rooms!.join!.containsKey(chat.id) ||
rooms!.join![chat.id]!.timeline?.events == null) {
return [];
}
for (final SyncRoomUpdate update in updates!) {
final List<dynamic>? events = getRoomUpdateEvents(type, update);
if (hasMembershipUpdate(
events,
type.name,
userId,
)) {
return true;
}
}
return false;
}
List<SyncRoomUpdate>? getRoomUpdates(Membership type) {
switch (type) {
case Membership.join:
return rooms?.join?.values.toList();
case Membership.leave:
return rooms?.leave?.values.toList();
case Membership.invite:
return rooms?.invite?.values.toList();
default:
return null;
}
}
bool isSpaceChildUpdate(String activeSpaceId) {
if (rooms?.join?.isEmpty ?? true) {
return false;
}
for (final update in rooms!.join!.entries) {
final String spaceId = update.key;
final List<MatrixEvent>? timelineEvents = update.value.timeline?.events;
final bool isUpdate = timelineEvents != null &&
spaceId == activeSpaceId &&
timelineEvents.any((event) => event.type == EventTypes.SpaceChild);
if (isUpdate) return true;
}
return false;
return rooms!.join![chat.id]!.timeline!.events!
.where(
(event) =>
event.type == EventTypes.Message &&
!event.eventId.startsWith("Pangea Chat"),
)
.map((event) => Event.fromMatrixEvent(event, chat))
.toList();
}
}
List<dynamic>? getRoomUpdateEvents(Membership type, SyncRoomUpdate update) {
switch (type) {
case Membership.join:
return (update as JoinedRoomUpdate).timeline?.events;
case Membership.leave:
return (update as LeftRoomUpdate).timeline?.events;
case Membership.invite:
return (update as InvitedRoomUpdate).inviteState;
default:
return null;
}
}
bool hasMembershipUpdate(
List<dynamic>? events,
String membershipType,
String userId,
) {
if (events == null) {
return false;
}
return events.any(
(event) =>
event.type == EventTypes.RoomMember &&
event.stateKey == userId &&
event.content['membership'] == membershipType,
);
}

View file

@ -0,0 +1,48 @@
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<String, dynamic> toJson() {
final data = <String, dynamic>{};
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,
);
}

View file

@ -0,0 +1,145 @@
import 'dart:async';
import 'package:fluffychat/pangea/constants/game_constants.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.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:matrix/matrix.dart';
/// A model of a game round. Manages the round's state and duration.
class GameRoundModel {
final Duration roundDuration = const Duration(
seconds: GameConstants.timerMaxSeconds,
);
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.
late DateTime createdAt;
Timer? timer;
StreamSubscription? syncSubscription;
final List<String> userMessageIDs = [];
final List<String> botMessageIDs = [];
GameRoundModel({
required this.room,
}) {
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);
}
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)
.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 hasNewBotMessage = botMessages.any(
(msg) => !botMessageIDs.contains(msg.eventId),
);
if (hasNewBotMessage) {
if (currentRoundStart == null) {
startRound();
} else {
endRound();
return;
}
}
if (currentRoundStart != null) {
for (final message in botMessages) {
if (!botMessageIDs.contains(message.eventId)) {
botMessageIDs.add(message.eventId);
}
}
for (final message in userMessages) {
if (!userMessageIDs.contains(message.eventId)) {
userMessageIDs.add(message.eventId);
}
}
}
}
/// Set the start and end times of the current and previous rounds.
Future<void> setRoundTimes({
DateTime? currentRoundStart,
DateTime? previousRoundEnd,
}) async {
final game = GameModel.fromJson(
room.getState(PangeaEventTypes.storyGame)?.content ?? {},
);
game.currentRoundStartTime = currentRoundStart;
game.previousRoundEndTime = previousRoundEnd;
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));
}
/// End and cleanup after the current round.
void endRound() {
syncSubscription?.cancel();
syncSubscription = null;
timer?.cancel();
timer = null;
setRoundTimes(
currentRoundStart: null,
previousRoundEnd: DateTime.now(),
);
}
void dispose() {
syncSubscription?.cancel();
syncSubscription = null;
timer?.cancel();
timer = null;
}
}

View file

@ -0,0 +1,138 @@
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;
const RoundTimer({
super.key,
required this.controller,
});
@override
RoundTimerState createState() => RoundTimerState();
}
class RoundTimerState extends State<RoundTimer> {
int currentSeconds = 0;
Timer? timer;
StreamSubscription? stateSubscription;
@override
void initState() {
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() {
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),
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,
// ),
// ),
],
),
],
),
),
),
);
}
}

View file

@ -184,9 +184,9 @@ class MatrixLocals extends MatrixLocalizations {
@override
String redactedAnEvent(Event redactedEvent) {
return l10n.redactedAnEvent(
redactedEvent.redactedBecause?.senderFromMemoryOrFallback
.calcDisplayname() ??
l10n.user,
// redactedEvent.redactedBecause?.senderFromMemoryOrFallback
// .calcDisplayname() ??
l10n.user,
);
}
@ -198,7 +198,8 @@ class MatrixLocals extends MatrixLocalizations {
@override
String removedBy(Event redactedEvent) {
return l10n.redactedBy(
redactedEvent.senderFromMemoryOrFallback.calcDisplayname(),
// redactedEvent.senderFromMemoryOrFallback.calcDisplayname(),
"?",
);
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
@ -20,17 +19,21 @@ extension RoomStatusExtension on Room {
} else if (typingUsers.length == 1) {
typingText = L10n.of(context)!.isTyping;
if (typingUsers.first.id != directChatMatrixID) {
typingText =
L10n.of(context)!.userIsTyping(typingUsers.first.calcDisplayname());
typingText = L10n.of(context)!.userIsTyping(
// typingUsers.first.calcDisplayname(),
"?",
);
}
} else if (typingUsers.length == 2) {
typingText = L10n.of(context)!.userAndUserAreTyping(
typingUsers.first.calcDisplayname(),
typingUsers[1].calcDisplayname(),
// typingUsers.first.calcDisplayname(),
// typingUsers[1].calcDisplayname(),
"?", "?",
);
} else if (typingUsers.length > 2) {
typingText = L10n.of(context)!.userAndOthersAreTyping(
typingUsers.first.calcDisplayname(),
// typingUsers.first.calcDisplayname(),
"?",
(typingUsers.length - 1).toString(),
);
}