diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 46f909e1f..9439b75ad 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -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 diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 5fcac8b57..490f6c91d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2303,6 +2303,12 @@ "user": {} } }, + "invitedBy": "📩 Invited by {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, "youInvitedUser": "📩 You invited {user}", "@youInvitedUser": { "placeholders": { @@ -4017,7 +4023,7 @@ "conversationBotTextAdventureZone_title": "Text Adventure", "conversationBotTextAdventureZone_instructionLabel": "Game Master Instructions", "conversationBotTextAdventureZone_instructionPlaceholder": "Set game master instructions", - "conversationBotCustomZone_instructionSystemPromptEmptyError": "Missing game master instructions", + "conversationBotCustomZone_instructionSystemPromptEmptyError": "Missing game master instructions", "studentAnalyticsNotAvailable": "Student data not currently available", "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 08db1a1bf..d16957134 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -14,16 +14,13 @@ 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/enum/message_mode_enum.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/overlay.dart'; @@ -118,12 +115,9 @@ class ChatController extends State 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; @@ -303,27 +297,6 @@ 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(); - }); - } - // Pangea# - @override void initState() { scrollController.addListener(_updateScrollController); @@ -339,7 +312,6 @@ class ChatController extends State sendingClient = Matrix.of(context).client; WidgetsBinding.instance.addObserver(this); // #Pangea - setRound(); if (!mounted) return; Future.delayed(const Duration(seconds: 1), () async { if (!mounted) return; @@ -564,7 +536,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 ccb8bf471..3510a4989 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'; @@ -28,21 +27,7 @@ class ChatEventList extends StatelessWidget { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; final events = controller.timeline!.events - .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# - , - ) + .where((event) => event.isVisibleInGui) .toList(); final animateInEventIndex = controller.animateInEventIndex; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 737b29534..27b68d4dc 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -12,7 +12,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'; @@ -118,10 +117,6 @@ class ChatView extends StatelessWidget { // ), // ]; return [ - RoundTimer(controller: controller), - const SizedBox( - width: 10, - ), ChatSettingsPopupMenu( controller.room, (!controller.room.isDirectChat && !controller.room.isArchived), diff --git a/lib/pages/chat/event_info_dialog.dart b/lib/pages/chat/event_info_dialog.dart index 8b8f1e703..38acdc84c 100644 --- a/lib/pages/chat/event_info_dialog.dart +++ b/lib/pages/chat/event_info_dialog.dart @@ -1,12 +1,14 @@ 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( @@ -47,16 +49,15 @@ class EventInfoDialog extends StatelessWidget { children: [ ListTile( leading: Avatar( - // mxContent: event.senderFromMemoryOrFallback.avatarUrl, - // name: event.senderFromMemoryOrFallback.calcDisplayname(), - name: "?", + mxContent: event.senderFromMemoryOrFallback.avatarUrl, + name: event.senderFromMemoryOrFallback.calcDisplayname(), 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), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 6a3b2078c..ae20d7a20 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -95,16 +95,12 @@ class MessageContent extends StatelessWidget { ListTile( contentPadding: EdgeInsets.zero, leading: Avatar( - // mxContent: sender.avatarUrl, - // name: sender.calcDisplayname(), - // presenceUserId: sender.stateKey, - name: "?", + mxContent: sender.avatarUrl, + name: sender.calcDisplayname(), + presenceUserId: sender.stateKey, client: event.room.client, ), - title: const Text( - // sender.calcDisplayname(), - "?", - ), + title: Text(sender.calcDisplayname()), subtitle: Text(event.originServerTs.localizedTime(context)), trailing: const Icon(Icons.lock_outlined), ), @@ -268,10 +264,9 @@ class MessageContent extends StatelessWidget { builder: (context, snapshot) { final reason = event.redactedBecause?.content.tryGet('reason'); - // final redactedBy = snapshot.data?.calcDisplayname() ?? - // event.redactedBecause?.senderId.localpart ?? - // L10n.of(context)!.user; - const redactedBy = "?"; + final redactedBy = snapshot.data?.calcDisplayname() ?? + event.redactedBecause?.senderId.localpart ?? + L10n.of(context)!.user; return _ButtonContent( label: reason == null ? L10n.of(context)!.redactedBy(redactedBy) @@ -375,9 +370,8 @@ 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: 'ℹ️', diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index a31765d38..b48f16cd9 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -59,8 +59,7 @@ 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( diff --git a/lib/pages/chat/seen_by_row.dart b/lib/pages/chat/seen_by_row.dart index 21fd6794e..af8b82069 100644 --- a/lib/pages/chat/seen_by_row.dart +++ b/lib/pages/chat/seen_by_row.dart @@ -37,10 +37,9 @@ class SeenByRow extends StatelessWidget { ? seenByUsers.sublist(0, maxAvatars) : seenByUsers) .map( - (user) => const Avatar( - // mxContent: user.avatarUrl, - // name: user.calcDisplayname(), - name: "?", + (user) => Avatar( + mxContent: user.avatarUrl, + name: user.calcDisplayname(), size: 16, ), ), diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index e02d59d1e..35fbf5d25 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -1,11 +1,12 @@ 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; @@ -55,11 +56,10 @@ class TypingIndicators extends StatelessWidget { child: Stack( children: [ if (typingUsers.isNotEmpty) - const Avatar( + Avatar( size: avatarSize, - // mxContent: typingUsers.first.avatarUrl, - // name: typingUsers.first.calcDisplayname(), - name: "?", + mxContent: typingUsers.first.avatarUrl, + name: typingUsers.first.calcDisplayname(), ), if (typingUsers.length == 2) Padding( @@ -69,10 +69,9 @@ class TypingIndicators extends StatelessWidget { mxContent: typingUsers.length == 2 ? typingUsers.last.avatarUrl : null, - // name: typingUsers.length == 2 - // ? typingUsers.last.calcDisplayname() - // : '+${typingUsers.length - 1}', - name: "?", + name: typingUsers.length == 2 + ? typingUsers.last.calcDisplayname() + : '+${typingUsers.length - 1}', ), ), ], diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 4f63a6238..9b5580692 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -39,10 +39,9 @@ class ParticipantListItem extends StatelessWidget { ), title: Row( children: [ - const Expanded( + Expanded( child: Text( - // user.calcDisplayname(), - "?", + user.calcDisplayname(), overflow: TextOverflow.ellipsis, ), ), @@ -89,9 +88,8 @@ class ParticipantListItem extends StatelessWidget { subtitle: Text(user.id), leading: Avatar( mxContent: user.avatarUrl, - // name: user.calcDisplayname(), - // presenceUserId: user.stateKey, - name: "?", + name: user.calcDisplayname(), + presenceUserId: user.stateKey, ), ), ); diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 54995d31d..76c75e83b 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,9 +1,11 @@ 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'; @@ -238,52 +240,51 @@ class ChatListItem extends StatelessWidget { softWrap: false, ) // #Pangea - : const SizedBox(), - // FutureBuilder( - // 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, - // ), - // ); - // }, - // ), + : FutureBuilder( + 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 diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index 68e627282..7542d6ae9 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -1,13 +1,15 @@ -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'; +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'; + class ChatSearchMessageTab extends StatelessWidget { final String searchQuery; final Room room; @@ -96,10 +98,9 @@ class ChatSearchMessageTab extends StatelessWidget { } final event = events[i]; final sender = event.senderFromMemoryOrFallback; - // final displayname = sender.calcDisplayname( - // i18n: MatrixLocals(L10n.of(context)!), - // ); - const displayname = "?"; + final displayname = sender.calcDisplayname( + i18n: MatrixLocals(L10n.of(context)!), + ); return _MessageSearchResultListTile( sender: sender, displayname: displayname, diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 7bd189288..ef79c0e31 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -22,10 +22,9 @@ 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)!; - const displayname = "?"; + final displayname = (user?.calcDisplayname() ?? + controller.widget.profile?.displayName ?? + controller.widget.profile?.userId.localpart)!; final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl; final client = Matrix.of(controller.widget.outerContext).client; @@ -40,7 +39,7 @@ class UserBottomSheetView extends StatelessWidget { title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text(displayname), + Text(displayname), PresenceBuilder( userId: userId, client: client, @@ -214,7 +213,7 @@ class UserBottomSheetView extends StatelessWidget { foregroundColor: Theme.of(context).colorScheme.onSurface, ), - label: const Text( + label: Text( displayname, maxLines: 1, overflow: TextOverflow.ellipsis, 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/extensions/sync_update_extension.dart b/lib/pangea/extensions/sync_update_extension.dart index 68c1f684a..6adb55c69 100644 --- a/lib/pangea/extensions/sync_update_extension.dart +++ b/lib/pangea/extensions/sync_update_extension.dart @@ -1,20 +1,85 @@ import 'package:matrix/matrix.dart'; extension MembershipUpdate on SyncUpdate { - List messages(Room chat) { - if (rooms?.join == null || - !rooms!.join!.containsKey(chat.id) || - rooms!.join![chat.id]!.timeline?.events == null) { - return []; + 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? updates = getRoomUpdates(type); + if (updates?.isEmpty ?? 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(); + for (final SyncRoomUpdate update in updates!) { + final List? events = getRoomUpdateEvents(type, update); + if (hasMembershipUpdate( + events, + type.name, + userId, + )) { + return true; + } + } + return false; + } + + List? 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? 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; } } + +List? 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? 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, + ); +} 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 deleted file mode 100644 index 3b43b86b5..000000000 --- a/lib/pangea/pages/games/story_game/round_model.dart +++ /dev/null @@ -1,145 +0,0 @@ -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 userMessageIDs = []; - final List 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 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; - } -} diff --git a/lib/pangea/widgets/chat/round_timer.dart b/lib/pangea/widgets/chat/round_timer.dart deleted file mode 100644 index 5153fee12..000000000 --- a/lib/pangea/widgets/chat/round_timer.dart +++ /dev/null @@ -1,138 +0,0 @@ -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 { - 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, - // ), - // ), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart index bfe1251dc..0134b50b3 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -14,9 +14,25 @@ Future getDatabaseCipher() async { String? password; try { - const secureStorage = FlutterSecureStorage(); + // #Pangea + // mogol/flutter_secure_storage#532 + // mogol/flutter_secure_storage#524 + // Pangea# + const secureStorage = FlutterSecureStorage( + // #Pangea + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + // Pangea# + ); + // #Pangea + await secureStorage.read(key: _passwordStorageKey); + // Pangea# final containsEncryptionKey = await secureStorage.read(key: _passwordStorageKey) != null; + // #Pangea + Sentry.addBreadcrumb( + Breadcrumb(message: 'containsEncryptionKey: $containsEncryptionKey'), + ); + // Pangea# if (!containsEncryptionKey) { final rng = Random.secure(); final list = Uint8List(32); @@ -29,18 +45,41 @@ Future getDatabaseCipher() async { } // workaround for if we just wrote to the key and it still doesn't exist password = await secureStorage.read(key: _passwordStorageKey); - if (password == null) throw MissingPluginException(); + if (password == null) { + throw MissingPluginException( + // #Pangea + "password is null after storing new password", + // Pangea# + ); + } } on MissingPluginException catch (e) { const FlutterSecureStorage() .delete(key: _passwordStorageKey) .catchError((_) {}); Logs().w('Database encryption is not supported on this platform', e); + // #Pangea + Sentry.addBreadcrumb( + Breadcrumb( + message: + 'Database encryption is not supported on this platform. Error message: ${e.message}', + data: {'exception': e}, + ), + ); + // Pangea# _sendNoEncryptionWarning(e); } catch (e, s) { const FlutterSecureStorage() .delete(key: _passwordStorageKey) .catchError((_) {}); Logs().w('Unable to init database encryption', e, s); + // #Pangea + Sentry.addBreadcrumb( + Breadcrumb( + message: 'Unable to init database encryption', + data: {'exception': e, 'stackTrace': s}, + ), + ); + // Pangea# _sendNoEncryptionWarning(e); } diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index f959898ed..cbba6b630 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -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,8 +198,7 @@ class MatrixLocals extends MatrixLocalizations { @override String removedBy(Event redactedEvent) { return l10n.redactedBy( - // redactedEvent.senderFromMemoryOrFallback.calcDisplayname(), - "?", + redactedEvent.senderFromMemoryOrFallback.calcDisplayname(), ); } @@ -347,8 +346,5 @@ class MatrixLocals extends MatrixLocalizations { l10n.startedKeyVerification(senderName); @override - String invitedBy(String senderName) { - // TODO: implement invitedBy - throw UnimplementedError(); - } + String invitedBy(String senderName) => l10n.invitedBy(senderName); } diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 277894473..02c70b088 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; + import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -19,21 +20,17 @@ 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(), ); }