diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 647697673..054190e8b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4010,9 +4010,9 @@ "wordsPerMinute": "Words per minute", "autoIGCToolName": "Run Language Assistance Automatically", "autoIGCToolDescription": "Automatically run language assistance after typing messages", - "runGrammarCorrection": "Run grammar correction", + "runGrammarCorrection": "Check message", "grammarCorrectionFailed": "Issues to address", - "grammarCorrectionComplete": "Grammar correction complete", + "grammarCorrectionComplete": "Looks good!", "leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.", "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.", "leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.", @@ -4059,5 +4059,12 @@ "practice": "Practice", "noLanguagesSet": "No languages set", "noActivitiesFound": "No practice activities found for this message", - "previous": "Previous" + "previous": "Previous", + "languageButtonLabel": "Language: {currentLanguage}", + "@languageButtonLabel": { + "type": "text", + "placeholders": { + "currentLanguage": {} + } + } } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index 0658bff50..dc328294a 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4609,9 +4609,9 @@ "enterNumber": "Introduzca un valor numérico entero.", "autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística", "autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes", - "runGrammarCorrection": "Corregir la gramática", + "runGrammarCorrection": "Comprobar mensaje", "grammarCorrectionFailed": "Cuestiones a tratar", - "grammarCorrectionComplete": "Corrección gramatical completa", + "grammarCorrectionComplete": "¡Se ve bien!", "leaveRoomDescription": "El chat se moverá al archivo. Los demás usuarios podrán ver que has abandonado el chat.", "archiveSpaceDescription": "Todos los chats de este espacio se moverán al archivo para ti y otros usuarios que no sean administradores.", "leaveSpaceDescription": "Todos los chats dentro de este espacio se moverán al archivo. Los demás usuarios podrán ver que has abandonado el espacio.", diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 2ff139a93..4f4e091d0 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -102,329 +102,444 @@ class ChatDetailsView extends StatelessWidget { backgroundColor: Theme.of(context).appBarTheme.backgroundColor, ), body: MaxWidthBody( - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), - itemBuilder: (BuildContext context, int i) => i == 0 - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.all(32.0), - child: Stack( - children: [ - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) - .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - Avatar.defaultSize * 2.5, + // #Pangea + // Chat description title has its own scrollbar so we disable the parent one + // otherwise they scroll with each other + child: ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith(scrollbars: false), + // Pangea# + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), + itemBuilder: (BuildContext context, int i) => i == 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: Stack( + children: [ + Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) + .appBarTheme + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + Avatar.defaultSize * 2.5, + ), ), - ), - child: Hero( - tag: controller - .widget.embeddedCloseButton != - null - ? 'embedded_content_banner' - : 'content_banner', - child: Avatar( - mxContent: room.avatar, - name: displayname, - size: Avatar.defaultSize * 2.5, + child: Hero( + tag: controller.widget + .embeddedCloseButton != + null + ? 'embedded_content_banner' + : 'content_banner', + child: Avatar( + mxContent: room.avatar, + name: displayname, + size: Avatar.defaultSize * 2.5, + ), ), ), - ), - if (!room.isDirectChat && - room.canChangeStateEvent( - EventTypes.RoomAvatar, - )) - Positioned( - bottom: 0, - right: 0, - child: FloatingActionButton.small( - onPressed: controller.setAvatarAction, - heroTag: null, - child: const Icon( - Icons.camera_alt_outlined, + if (!room.isDirectChat && + room.canChangeStateEvent( + EventTypes.RoomAvatar, + )) + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton.small( + onPressed: controller.setAvatarAction, + heroTag: null, + child: const Icon( + Icons.camera_alt_outlined, + ), ), ), - ), - ], + ], + ), ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton.icon( - onPressed: () => room.isDirectChat - ? null - : room.canChangeStateEvent( - EventTypes.RoomName, - ) - ? controller.setDisplaynameAction() - : FluffyShare.share( - displayname, - context, - copyOnly: true, - ), - icon: Icon( - room.isDirectChat - ? Icons.chat_bubble_outline + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => room.isDirectChat + ? null : room.canChangeStateEvent( EventTypes.RoomName, ) - ? Icons.edit_outlined - : Icons.copy_outlined, - size: 16, - ), - style: TextButton.styleFrom( - foregroundColor: Theme.of(context) - .colorScheme - .onSurface, - ), - label: Text( - room.isDirectChat - ? L10n.of(context)!.directChat - : displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: const TextStyle(fontSize: 18), - ), - ), - TextButton.icon( - onPressed: () => room.isDirectChat - ? null - : context.push( - '/rooms/${controller.roomId}/details/members', - ), - icon: const Icon( - Icons.group_outlined, - size: 14, - ), - style: TextButton.styleFrom( - foregroundColor: Theme.of(context) - .colorScheme - .secondary, + ? controller + .setDisplaynameAction() + : FluffyShare.share( + displayname, + context, + copyOnly: true, + ), + icon: Icon( + room.isDirectChat + ? Icons.chat_bubble_outline + : room.canChangeStateEvent( + EventTypes.RoomName, + ) + ? Icons.edit_outlined + : Icons.copy_outlined, + size: 16, + ), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context) + .colorScheme + .onSurface, + ), + label: Text( + room.isDirectChat + ? L10n.of(context)!.directChat + : displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 18), + ), ), - label: Text( - L10n.of(context)!.countParticipants( - actualMembersCount, + TextButton.icon( + onPressed: () => room.isDirectChat + ? null + : context.push( + '/rooms/${controller.roomId}/details/members', + ), + icon: const Icon( + Icons.group_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context) + .colorScheme + .secondary, + ), + label: Text( + L10n.of(context)!.countParticipants( + actualMembersCount, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 12), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: const TextStyle(fontSize: 12), ), - ), - ], + ], + ), ), + ], + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor, + ), + // if (room.canSendEvent('m.room.name')) + if (room.isRoomAdmin) + // #Pangea + ClassNameButton( + room: room, + controller: controller, + ), + if (room.canSendEvent('m.room.topic')) + ClassDescriptionButton( + room: room, + controller: controller, ), - ], - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor, - ), - // if (room.canSendEvent('m.room.name')) - if (room.isRoomAdmin) // #Pangea - ClassNameButton( - room: room, - controller: controller, - ), - if (room.canSendEvent('m.room.topic')) - ClassDescriptionButton( + RoomCapacityButton( room: room, controller: controller, ), - // #Pangea - RoomCapacityButton( - room: room, - controller: controller, - ), - // Pangea# - if (room.isSpace && room.isRoomAdmin) - ListTile( - title: Text( - L10n.of(context)!.spaceAnalytics, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, + // Pangea# + if (room.isSpace && room.isRoomAdmin) + ListTile( + title: Text( + L10n.of(context)!.spaceAnalytics, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), ), - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.analytics_outlined, + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.analytics_outlined, + ), + ), + onTap: () => context.go( + '/rooms/analytics/${room.id}', ), ), - onTap: () => context.go( - '/rooms/analytics/${room.id}', + // commenting out language settings in spaces for now + // if (room.languageSettings != null && room.isRoomAdmin) + // LanguageSettings( + // roomId: controller.roomId, + // startOpen: false, + // ), + if (room.pangeaRoomRules != null) + RoomRulesEditor( + roomId: controller.roomId, + startOpen: false, ), + // if (!room.canChangeStateEvent(EventTypes.RoomTopic)) + // ListTile( + // title: Text( + // L10n.of(context)!.chatDescription, + // style: TextStyle( + // color: Theme.of(context).colorScheme.secondary, + // fontWeight: FontWeight.bold, + // ), + // ), + // ) + // else + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: TextButton.icon( + // onPressed: controller.setTopicAction, + // label: Text(L10n.of(context)!.setChatDescription), + // icon: const Icon(Icons.edit_outlined), + // style: TextButton.styleFrom( + // backgroundColor: Theme.of(context) + // .colorScheme + // .secondaryContainer, + // foregroundColor: Theme.of(context) + // .colorScheme + // .onSecondaryContainer, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // ), + // child: SelectableLinkify( + // text: room.topic.isEmpty + // ? L10n.of(context)!.noChatDescriptionYet + // : room.topic, + // options: const LinkifyOptions(humanize: false), + // linkStyle: const TextStyle( + // color: Colors.blueAccent, + // decorationColor: Colors.blueAccent, + // ), + // style: TextStyle( + // fontSize: 14, + // fontStyle: room.topic.isEmpty + // ? FontStyle.italic + // : FontStyle.normal, + // color: + // Theme.of(context).textTheme.bodyMedium!.color, + // decorationColor: + // Theme.of(context).textTheme.bodyMedium!.color, + // ), + // onOpen: (url) => + // UrlLauncher(context, url.url).launchUrl(), + // ), + // ), + // const SizedBox(height: 16), + // Divider( + // height: 1, + // color: Theme.of(context).dividerColor, + // ), + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon( + // Icons.insert_emoticon_outlined, + // ), + // ), + // title: + // Text(L10n.of(context)!.customEmojisAndStickers), + // subtitle: Text(L10n.of(context)!.setCustomEmotes), + // onTap: controller.goToEmoteSettings, + // trailing: const Icon(Icons.chevron_right_outlined), + // ), + // if (!room.isDirectChat) + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon(Icons.shield_outlined), + // ), + // title: Text( + // L10n.of(context)!.accessAndVisibility, + // ), + // subtitle: Text( + // L10n.of(context)!.accessAndVisibilityDescription, + // ), + // onTap: () => context + // .push('/rooms/${room.id}/details/access'), + // trailing: const Icon(Icons.chevron_right_outlined), + // ), + // if (!room.isDirectChat) + if (!room.isDirectChat && + !room.isSpace && + room.isRoomAdmin) + // Pangea# + ListTile( + // #Pangea + // title: Text(L10n.of(context)!.chatPermissions), + title: Text( + L10n.of(context)!.editChatPermissions, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + // Pangea# + subtitle: Text( + L10n.of(context)!.whoCanPerformWhichAction, + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.edit_attributes_outlined, + ), + ), + // #Pangea + // trailing: const Icon(Icons.chevron_right_outlined), + // Pangea# + onTap: () => context.push( + '/rooms/${room.id}/details/permissions', + ), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor, ), - // commenting out language settings in spaces for now - // if (room.languageSettings != null && room.isRoomAdmin) - // LanguageSettings( - // roomId: controller.roomId, - // startOpen: false, - // ), - if (room.pangeaRoomRules != null) - RoomRulesEditor( - roomId: controller.roomId, - startOpen: false, - ), - // if (!room.canChangeStateEvent(EventTypes.RoomTopic)) - // ListTile( - // title: Text( - // L10n.of(context)!.chatDescription, - // style: TextStyle( - // color: Theme.of(context).colorScheme.secondary, - // fontWeight: FontWeight.bold, - // ), - // ), - // ) - // else - // Padding( - // padding: const EdgeInsets.all(16.0), - // child: TextButton.icon( - // onPressed: controller.setTopicAction, - // label: Text(L10n.of(context)!.setChatDescription), - // icon: const Icon(Icons.edit_outlined), - // style: TextButton.styleFrom( - // backgroundColor: Theme.of(context) - // .colorScheme - // .secondaryContainer, - // foregroundColor: Theme.of(context) - // .colorScheme - // .onSecondaryContainer, - // ), - // ), - // ), - // Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 16.0, - // ), - // child: SelectableLinkify( - // text: room.topic.isEmpty - // ? L10n.of(context)!.noChatDescriptionYet - // : room.topic, - // options: const LinkifyOptions(humanize: false), - // linkStyle: const TextStyle( - // color: Colors.blueAccent, - // decorationColor: Colors.blueAccent, - // ), - // style: TextStyle( - // fontSize: 14, - // fontStyle: room.topic.isEmpty - // ? FontStyle.italic - // : FontStyle.normal, - // color: - // Theme.of(context).textTheme.bodyMedium!.color, - // decorationColor: - // Theme.of(context).textTheme.bodyMedium!.color, - // ), - // onOpen: (url) => - // UrlLauncher(context, url.url).launchUrl(), - // ), - // ), - // const SizedBox(height: 16), - // Divider( - // height: 1, - // color: Theme.of(context).dividerColor, - // ), - // ListTile( - // leading: CircleAvatar( - // backgroundColor: - // Theme.of(context).scaffoldBackgroundColor, - // foregroundColor: iconColor, - // child: const Icon( - // Icons.insert_emoticon_outlined, - // ), - // ), - // title: - // Text(L10n.of(context)!.customEmojisAndStickers), - // subtitle: Text(L10n.of(context)!.setCustomEmotes), - // onTap: controller.goToEmoteSettings, - // trailing: const Icon(Icons.chevron_right_outlined), - // ), - // if (!room.isDirectChat) - // ListTile( - // leading: CircleAvatar( - // backgroundColor: - // Theme.of(context).scaffoldBackgroundColor, - // foregroundColor: iconColor, - // child: const Icon(Icons.shield_outlined), - // ), - // title: Text( - // L10n.of(context)!.accessAndVisibility, - // ), - // subtitle: Text( - // L10n.of(context)!.accessAndVisibilityDescription, - // ), - // onTap: () => context - // .push('/rooms/${room.id}/details/access'), - // trailing: const Icon(Icons.chevron_right_outlined), - // ), - // if (!room.isDirectChat) - if (!room.isDirectChat && - !room.isSpace && - room.isRoomAdmin) - // Pangea# - ListTile( - // #Pangea - // title: Text(L10n.of(context)!.chatPermissions), - title: Text( - L10n.of(context)!.editChatPermissions, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, + // #Pangea + if (room.canInvite && + !room.isDirectChat && + (!room.isSpace || room.isRoomAdmin)) + ListTile( + title: Text( + room.isSpace + ? L10n.of(context)!.inviteUsersFromPangea + : L10n.of(context)!.inviteStudentByUserName, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context) + .textTheme + .bodyLarge! + .color, + child: const Icon( + Icons.add, + ), ), + onTap: () => + context.go('/rooms/${room.id}/invite'), ), - // Pangea# - subtitle: Text( - L10n.of(context)!.whoCanPerformWhichAction, + if (room.showClassEditOptions && room.isSpace) + SpaceDetailsToggleAddStudentsTile( + controller: controller, ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.edit_attributes_outlined, - ), + if (controller.displayAddStudentOptions && + room.showClassEditOptions) + ClassInvitationButtons(roomId: controller.roomId!), + const Divider(height: 1), + if (!room.isSpace && + !room.isDirectChat && + room.canInvite) + ConversationBotSettings( + key: controller.addConversationBotKey, + room: room, ), - // #Pangea - // trailing: const Icon(Icons.chevron_right_outlined), - // Pangea# - onTap: () => context - .push('/rooms/${room.id}/details/permissions'), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor, - ), - // #Pangea - if (room.canInvite && - !room.isDirectChat && - (!room.isSpace || room.isRoomAdmin)) + const Divider(height: 1), + if (!room.isDirectChat && room.isRoomAdmin) + AddToSpaceToggles( + roomId: room.id, + key: controller.addToSpaceKey, + startOpen: false, + ), + const Divider(height: 1), + if (!room.isDirectChat) + if (room.isRoomAdmin) + ListTile( + title: Text( + room.isSpace + ? L10n.of(context)!.archiveSpace + : L10n.of(context)!.archive, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.archive_outlined, + ), + ), + onTap: () async { + OkCancelResult confirmed = OkCancelResult.ok; + bool shouldGo = false; + // archiveSpace has its own popup; only show if not space + if (!room.isSpace) { + confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context)! + .archiveRoomDescription, + ); + } + if (confirmed == OkCancelResult.ok) { + if (room.isSpace) { + shouldGo = await room.archiveSpace( + context, + Matrix.of(context).client, + ); + } else { + final success = + await showFutureLoadingDialog( + context: context, + future: () async { + await room.archive(); + }, + ); + shouldGo = (success.error == null); + } + if (shouldGo) { + context.go('/rooms'); + } + } + }, + ), ListTile( title: Text( - room.isSpace - ? L10n.of(context)!.inviteUsersFromPangea - : L10n.of(context)!.inviteStudentByUserName, + L10n.of(context)!.leave, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, @@ -433,248 +548,151 @@ class ChatDetailsView extends StatelessWidget { leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - Theme.of(context).textTheme.bodyLarge!.color, + foregroundColor: iconColor, child: const Icon( - Icons.add, + Icons.arrow_forward, ), ), - onTap: () => context.go('/rooms/${room.id}/invite'), - ), - if (room.showClassEditOptions && room.isSpace) - SpaceDetailsToggleAddStudentsTile( - controller: controller, - ), - if (controller.displayAddStudentOptions && - room.showClassEditOptions) - ClassInvitationButtons(roomId: controller.roomId!), - const Divider(height: 1), - if (!room.isSpace && - !room.isDirectChat && - room.canInvite) - ConversationBotSettings( - key: controller.addConversationBotKey, - room: room, - ), - const Divider(height: 1), - if (!room.isDirectChat && room.isRoomAdmin) - AddToSpaceToggles( - roomId: room.id, - key: controller.addToSpaceKey, - startOpen: false, + onTap: () async { + OkCancelResult confirmed = OkCancelResult.ok; + bool shouldGo = false; + // If user is only admin, room will be archived + final bool onlyAdmin = await room.isOnlyAdmin(); + // archiveSpace has its own popup; only show if not space + if (!room.isSpace) { + confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + message: onlyAdmin + ? L10n.of(context)!.onlyAdminDescription + : L10n.of(context)!.leaveRoomDescription, + ); + } + if (confirmed == OkCancelResult.ok) { + if (room.isSpace) { + shouldGo = onlyAdmin + ? await room.archiveSpace( + context, + Matrix.of(context).client, + onlyAdmin: true, + ) + : await room.leaveSpace( + context, + Matrix.of(context).client, + ); + } else { + final success = await showFutureLoadingDialog( + context: context, + future: () async { + onlyAdmin + ? await room.archive() + : await room.leave(); + }, + ); + shouldGo = (success.error == null); + } + if (shouldGo) { + context.go('/rooms'); + } + } + }, ), - const Divider(height: 1), - if (!room.isDirectChat) - if (room.isRoomAdmin) - ListTile( + if (room.isRoomAdmin && !room.isDirectChat) + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, title: Text( room.isSpace - ? L10n.of(context)!.archiveSpace - : L10n.of(context)!.archive, + ? L10n.of(context)!.lockSpace + : L10n.of(context)!.lockChat, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), - leading: CircleAvatar( + secondary: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, foregroundColor: iconColor, - child: const Icon( - Icons.archive_outlined, + child: Icon( + room.isLocked + ? Icons.lock_outlined + : Icons.no_encryption_outlined, ), ), - onTap: () async { - OkCancelResult confirmed = OkCancelResult.ok; - bool shouldGo = false; - // archiveSpace has its own popup; only show if not space - if (!room.isSpace) { - confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)! - .archiveRoomDescription, - ); - } - if (confirmed == OkCancelResult.ok) { - if (room.isSpace) { - shouldGo = await room.archiveSpace( - context, - Matrix.of(context).client, - ); - } else { - final success = - await showFutureLoadingDialog( - context: context, - future: () async { - await room.archive(); - }, - ); - shouldGo = (success.error == null); - } - if (shouldGo) { - context.go('/rooms'); - } - } - }, - ), - ListTile( - title: Text( - L10n.of(context)!.leave, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.arrow_forward, - ), - ), - onTap: () async { - OkCancelResult confirmed = OkCancelResult.ok; - bool shouldGo = false; - // If user is only admin, room will be archived - final bool onlyAdmin = await room.isOnlyAdmin(); - // archiveSpace has its own popup; only show if not space - if (!room.isSpace) { - confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, + value: room.isLocked, + onChanged: (value) => showFutureLoadingDialog( context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: onlyAdmin - ? L10n.of(context)!.onlyAdminDescription - : L10n.of(context)!.leaveRoomDescription, - ); - } - if (confirmed == OkCancelResult.ok) { - if (room.isSpace) { - shouldGo = onlyAdmin - ? await room.archiveSpace( - context, + future: () => value + ? lockRoom( + room, Matrix.of(context).client, - onlyAdmin: true, ) - : await room.leaveSpace( - context, + : unlockRoom( + room, Matrix.of(context).client, - ); - } else { - final success = await showFutureLoadingDialog( - context: context, - future: () async { - onlyAdmin - ? await room.archive() - : await room.leave(); - }, - ); - shouldGo = (success.error == null); - } - if (shouldGo) { - context.go('/rooms'); - } - } - }, - ), - if (room.isRoomAdmin && !room.isDirectChat) - SwitchListTile.adaptive( - activeColor: AppConfig.activeToggleColor, + ), + ), + ), + const Divider(height: 1), + // Pangea# + ListTile( title: Text( - room.isSpace - ? L10n.of(context)!.lockSpace - : L10n.of(context)!.lockChat, + L10n.of(context)!.countParticipants( + actualMembersCount.toString(), + ), style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), - secondary: CircleAvatar( + ), + // #Pangea + // if (!room.isDirectChat && room.canInvite) + // ListTile( + // title: Text(L10n.of(context)!.inviteContact), + // leading: CircleAvatar( + // backgroundColor: Theme.of(context) + // .colorScheme + // .primaryContainer, + // foregroundColor: Theme.of(context) + // .colorScheme + // .onPrimaryContainer, + // radius: Avatar.defaultSize / 2, + // child: const Icon(Icons.add_outlined), + // ), + // trailing: const Icon(Icons.chevron_right_outlined), + // onTap: () => context.go('/rooms/${room.id}/invite'), + // ), + // Pangea# + ], + ) + : i < members.length + 1 + ? ParticipantListItem(members[i - 1]) + : ListTile( + title: Text( + L10n.of(context)!.loadCountMoreParticipants( + (actualMembersCount - members.length) + .toString(), + ), + ), + leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: Icon( - room.isLocked - ? Icons.lock_outlined - : Icons.no_encryption_outlined, + child: const Icon( + Icons.group_outlined, + color: Colors.grey, ), ), - value: room.isLocked, - onChanged: (value) => showFutureLoadingDialog( - context: context, - future: () => value - ? lockRoom( - room, - Matrix.of(context).client, - ) - : unlockRoom( - room, - Matrix.of(context).client, - ), - ), - ), - const Divider(height: 1), - // Pangea# - ListTile( - title: Text( - L10n.of(context)!.countParticipants( - actualMembersCount.toString(), - ), - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, + onTap: () => context.push( + '/rooms/${controller.roomId!}/details/members', ), + trailing: const Icon(Icons.chevron_right_outlined), ), - ), - // #Pangea - // if (!room.isDirectChat && room.canInvite) - // ListTile( - // title: Text(L10n.of(context)!.inviteContact), - // leading: CircleAvatar( - // backgroundColor: Theme.of(context) - // .colorScheme - // .primaryContainer, - // foregroundColor: Theme.of(context) - // .colorScheme - // .onPrimaryContainer, - // radius: Avatar.defaultSize / 2, - // child: const Icon(Icons.add_outlined), - // ), - // trailing: const Icon(Icons.chevron_right_outlined), - // onTap: () => context.go('/rooms/${room.id}/invite'), - // ), - // Pangea# - ], - ) - : i < members.length + 1 - ? ParticipantListItem(members[i - 1]) - : ListTile( - title: Text( - L10n.of(context)!.loadCountMoreParticipants( - (actualMembersCount - members.length).toString(), - ), - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - child: const Icon( - Icons.group_outlined, - color: Colors.grey, - ), - ), - onTap: () => context.push( - '/rooms/${controller.roomId!}/details/members', - ), - trailing: const Icon(Icons.chevron_right_outlined), - ), + ), ), ), ); diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart index ea81c842c..2b1dff187 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -35,11 +35,26 @@ class ChatPermissionsSettingsView extends StatelessWidget { final powerLevels = Map.from(powerLevelsContent) // #Pangea // ..removeWhere((k, v) => v is! int); - ..removeWhere((k, v) => v is! int || k.equals("m.call.invite")); + ..removeWhere( + (k, v) => + v is! int || + k.equals("m.call.invite") || + k.equals("historical") || + k.equals("state_default"), + ); // Pangea# final eventsPowerLevels = Map.from( powerLevelsContent.tryGetMap('events') ?? {}, - )..removeWhere((k, v) => v is! int); + // #Pangea + )..removeWhere( + (k, v) => + v is! int || + k.equals("m.space.child") || + k.equals("pangea.usranalytics") || + k.equals(EventTypes.RoomPowerLevels), + ); + // )..removeWhere((k, v) => v is! int); + // Pangea# return Column( children: [ Column( @@ -57,51 +72,59 @@ class ChatPermissionsSettingsView extends StatelessWidget { ), canEdit: room.canChangePowerLevel, ), - Divider(color: Theme.of(context).dividerColor), - ListTile( - title: Text( - L10n.of(context)!.notifications, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - Builder( - builder: (context) { - const key = 'rooms'; - final value = powerLevelsContent - .containsKey('notifications') - ? powerLevelsContent - .tryGetMap('notifications') - ?.tryGet('rooms') ?? - 0 - : 0; - return PermissionsListTile( - permissionKey: key, - permission: value, - category: 'notifications', - canEdit: room.canChangePowerLevel, - onChanged: (level) => controller.editPowerLevel( - context, - key, - value, - newLevel: level, - category: 'notifications', + // #Pangea + // Why would teacher need to stop students from seeing notifications? + // Divider(color: Theme.of(context).dividerColor), + // ListTile( + // title: Text( + // L10n.of(context)!.notifications, + // style: TextStyle( + // color: Theme.of(context).colorScheme.primary, + // fontWeight: FontWeight.bold, + // ), + // ), + // ), + // Builder( + // builder: (context) { + // const key = 'rooms'; + // final value = powerLevelsContent + // .containsKey('notifications') + // ? powerLevelsContent + // .tryGetMap('notifications') + // ?.tryGet('rooms') ?? + // 0 + // : 0; + // return PermissionsListTile( + // permissionKey: key, + // permission: value, + // category: 'notifications', + // canEdit: room.canChangePowerLevel, + // onChanged: (level) => controller.editPowerLevel( + // context, + // key, + // value, + // newLevel: level, + // category: 'notifications', + // ), + // ); + // }, + // ), + // Only show if there are actually items in this category + if (eventsPowerLevels.isNotEmpty) + // Pangea# + Divider(color: Theme.of(context).dividerColor), + // #Pangea + if (eventsPowerLevels.isNotEmpty) + // Pangea# + ListTile( + title: Text( + L10n.of(context)!.configureChat, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, ), - ); - }, - ), - Divider(color: Theme.of(context).dividerColor), - ListTile( - title: Text( - L10n.of(context)!.configureChat, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, ), ), - ), for (final entry in eventsPowerLevels.entries) PermissionsListTile( permissionKey: entry.key, diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 6ea029a6f..a0c6a5f6b 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:fluffychat/pangea/enum/edit_type.dart'; import 'package:fluffychat/pangea/models/it_step.dart'; import 'package:fluffychat/pangea/models/language_detection_model.dart'; @@ -570,13 +571,3 @@ class Choreographer { return AssistanceState.complete; } } - -// assistance state is, user has not typed a message, user has typed a message and IGC has not run, -// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done -enum AssistanceState { - noMessage, - notFetched, - fetching, - fetched, - complete, -} diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 49e4b078d..877158dd2 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -1,10 +1,9 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/constants/colors.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -54,15 +53,15 @@ class StartIGCButtonState extends State setState(() => prevState = assistanceState); } + bool get itEnabled => widget.controller.choreographer.itEnabled; + bool get igcEnabled => widget.controller.choreographer.igcEnabled; + CanSendStatus get canSendStatus => + widget.controller.pangeaController.subscriptionController.canSendStatus; + bool get grammarCorrectionEnabled => + (itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed; + @override Widget build(BuildContext context) { - final bool itEnabled = widget.controller.choreographer.itEnabled; - final bool igcEnabled = widget.controller.choreographer.igcEnabled; - final CanSendStatus canSendStatus = - widget.controller.pangeaController.subscriptionController.canSendStatus; - final bool grammarCorrectionEnabled = - (itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed; - if (!grammarCorrectionEnabled || widget.controller.choreographer.isAutoIGCEnabled || widget.controller.choreographer.choreoMode == ChoreoMode.it) { @@ -89,7 +88,7 @@ class StartIGCButtonState extends State disabledElevation: 0, shape: const CircleBorder(), onPressed: () { - if (assistanceState != AssistanceState.complete) { + if (assistanceState != AssistanceState.fetching) { widget.controller.choreographer .getLanguageHelp( false, @@ -142,32 +141,3 @@ class StartIGCButtonState extends State ); } } - -extension AssistanceStateExtension on AssistanceState { - Color stateColor(context) { - switch (this) { - case AssistanceState.noMessage: - case AssistanceState.notFetched: - case AssistanceState.fetching: - return Theme.of(context).colorScheme.primary; - case AssistanceState.fetched: - return PangeaColors.igcError; - case AssistanceState.complete: - return AppConfig.success; - } - } - - String tooltip(L10n l10n) { - switch (this) { - case AssistanceState.noMessage: - case AssistanceState.notFetched: - return l10n.runGrammarCorrection; - case AssistanceState.fetching: - return ""; - case AssistanceState.fetched: - return l10n.grammarCorrectionFailed; - case AssistanceState.complete: - return l10n.grammarCorrectionComplete; - } - } -} diff --git a/lib/pangea/controllers/language_list_controller.dart b/lib/pangea/controllers/language_list_controller.dart index 345d5b5e7..59c3cce88 100644 --- a/lib/pangea/controllers/language_list_controller.dart +++ b/lib/pangea/controllers/language_list_controller.dart @@ -27,7 +27,7 @@ class PangeaLanguage { static Future initialize() async { try { - _langList = await _getCahedFlags(); + _langList = await _getCachedFlags(); if (await _shouldFetch || _langList.isEmpty) { _langList = await LanguageRepo.fetchLanguages(); @@ -77,7 +77,7 @@ class PangeaLanguage { await MyShared.saveJson(PrefKey.flags, flagMap); } - static Future> _getCahedFlags() async { + static Future> _getCachedFlags() async { final Map? flagsMap = await MyShared.readJson(PrefKey.flags); if (flagsMap == null) { diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 7083efbfd..1475c0e6e 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -62,12 +62,13 @@ class AnalyticsController extends BaseController { timeSpan.toString(), local: true, ); + setState(); } ///////// SPACE ANALYTICS LANGUAGES ////////// String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY"; - LanguageModel get currentAnalyticsSpaceLang { + LanguageModel get currentAnalyticsLang { try { final String? str = _pangeaController.pStoreService.read( _analyticsSpaceLangKey, @@ -83,41 +84,43 @@ class AnalyticsController extends BaseController { } } - Future setCurrentAnalyticsSpaceLang(LanguageModel lang) async { + Future setCurrentAnalyticsLang(LanguageModel lang) async { await _pangeaController.pStoreService.save( _analyticsSpaceLangKey, lang.langCode, local: true, ); + setState(); } + /// given an analytics event type and the current analytics language, + /// get the last time the user updated their analytics Future myAnalyticsLastUpdated(String type) async { - // given an analytics event type, get the last updated times - // for each of the user's analytics rooms and return the most recent - // Most Recent instead of the oldest because, for instance: - // My last Spanish event was sent 3 days ago. - // My last English event was sent 1 day ago. - // When I go to check if the cached data is out of date, the cached item was set 2 days ago. - // I know there’s new data available because the English update data (the most recent) is after the cache’s creation time. - // So, I should update the cache. final List analyticsRooms = _pangeaController .matrixState.client.allMyAnalyticsRooms .where((room) => room.isAnalyticsRoom) .toList(); - final List lastUpdates = []; + final Map langCodeLastUpdates = {}; for (final Room analyticsRoom in analyticsRooms) { + final String? roomLang = analyticsRoom.madeForLang; + if (roomLang == null) continue; final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( type, _pangeaController.matrixState.client.userID!, ); if (lastUpdated != null) { - lastUpdates.add(lastUpdated); + langCodeLastUpdates[roomLang] = lastUpdated; } } - if (lastUpdates.isEmpty) return null; - return lastUpdates.reduce( + if (langCodeLastUpdates.isEmpty) return null; + final String? l2Code = + _pangeaController.languageController.userL2?.langCode; + if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) { + return langCodeLastUpdates[l2Code]; + } + return langCodeLastUpdates.values.reduce( (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, ); } @@ -134,7 +137,7 @@ class AnalyticsController extends BaseController { final List> lastUpdatedFutures = []; for (final student in space.students) { final Room? analyticsRoom = _pangeaController.matrixState.client - .analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id); + .analyticsRoomLocal(currentAnalyticsLang.langCode, student.id); if (analyticsRoom == null) continue; lastUpdatedFutures.add( analyticsRoom.analyticsLastUpdated( @@ -177,28 +180,20 @@ class AnalyticsController extends BaseController { //////////////////////////// MESSAGE SUMMARY ANALYTICS //////////////////////////// + /// get all the summary analytics events for the current user + /// in the current language's analytics room Future> mySummaryAnalytics() async { - // gets all the summary analytics events for the user - // since the current timespace's cut off date - final analyticsRooms = - _pangeaController.matrixState.client.allMyAnalyticsRooms; - - final List allEvents = []; - - // TODO switch to using list of futures - for (final Room analyticsRoom in analyticsRooms) { - final List? roomEvents = - await analyticsRoom.getAnalyticsEvents( - type: PangeaEventTypes.summaryAnalytics, - since: currentAnalyticsTimeSpan.cutOffDate, - userId: _pangeaController.matrixState.client.userID!, - ); - - allEvents.addAll( - roomEvents?.cast() ?? [], - ); - } - return allEvents; + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(currentAnalyticsLang.langCode); + if (analyticsRoom == null) return []; + + final List? roomEvents = + await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.summaryAnalytics, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: _pangeaController.matrixState.client.userID!, + ); + return roomEvents?.cast() ?? []; } Future> spaceMemberAnalytics( @@ -216,7 +211,7 @@ class AnalyticsController extends BaseController { final List analyticsEvents = []; for (final student in space.students) { final Room? analyticsRoom = _pangeaController.matrixState.client - .analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id); + .analyticsRoomLocal(currentAnalyticsLang.langCode, student.id); if (analyticsRoom != null) { final List? roomEvents = @@ -261,7 +256,7 @@ class AnalyticsController extends BaseController { (e.defaultSelected.type == defaultSelected.type) && (e.selected?.id == selected?.id) && (e.selected?.type == selected?.type) && - (e.langCode == currentAnalyticsSpaceLang.langCode), + (e.langCode == currentAnalyticsLang.langCode), ); if (index != -1) { @@ -289,7 +284,7 @@ class AnalyticsController extends BaseController { chartAnalyticsModel: chartAnalyticsModel, defaultSelected: defaultSelected, selected: selected, - langCode: currentAnalyticsSpaceLang.langCode, + langCode: currentAnalyticsLang.langCode, ), ); } @@ -525,20 +520,18 @@ class AnalyticsController extends BaseController { //////////////////////////// CONSTRUCTS //////////////////////////// Future> allMyConstructs() async { - final List analyticsRooms = - _pangeaController.matrixState.client.allMyAnalyticsRooms; - - final List allConstructs = []; - for (final Room analyticsRoom in analyticsRooms) { - final List? roomEvents = - (await analyticsRoom.getAnalyticsEvents( - type: PangeaEventTypes.construct, - since: currentAnalyticsTimeSpan.cutOffDate, - userId: _pangeaController.matrixState.client.userID!, - )) - ?.cast(); - allConstructs.addAll(roomEvents ?? []); - } + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(currentAnalyticsLang.langCode); + if (analyticsRoom == null) return []; + + final List? roomEvents = + (await analyticsRoom.getAnalyticsEvents( + type: PangeaEventTypes.construct, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: _pangeaController.matrixState.client.userID!, + )) + ?.cast(); + final List allConstructs = roomEvents ?? []; final List adminSpaceRooms = await _pangeaController.matrixState.client.teacherRoomIds; @@ -561,7 +554,7 @@ class AnalyticsController extends BaseController { final List constructEvents = []; for (final student in space.students) { final Room? analyticsRoom = _pangeaController.matrixState.client - .analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id); + .analyticsRoomLocal(currentAnalyticsLang.langCode, student.id); if (analyticsRoom != null) { final List? roomEvents = (await analyticsRoom.getAnalyticsEvents( @@ -661,7 +654,7 @@ class AnalyticsController extends BaseController { e.defaultSelected.type == defaultSelected.type && e.selected?.id == selected?.id && e.selected?.type == selected?.type && - e.langCode == currentAnalyticsSpaceLang.langCode, + e.langCode == currentAnalyticsLang.langCode, ); if (index > -1) { @@ -687,7 +680,7 @@ class AnalyticsController extends BaseController { events: List.from(events), defaultSelected: defaultSelected, selected: selected, - langCode: currentAnalyticsSpaceLang.langCode, + langCode: currentAnalyticsLang.langCode, ); _cachedConstructs.add(entry); } diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index ea0d06c56..614fcf9db 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -25,20 +25,23 @@ class MyAnalyticsController extends BaseController { final int _maxMessagesCached = 10; final int _minutesBeforeUpdate = 5; + /// the time since the last update that will trigger an automatic update + final Duration _timeSinceUpdate = const Duration(days: 1); + MyAnalyticsController(PangeaController pangeaController) { _pangeaController = pangeaController; } - // adds the listener that handles when to run automatic updates - // to analytics - either after a certain number of messages sent/ - // received or after a certain amount of time without an update + /// adds the listener that handles when to run automatic updates + /// to analytics - either after a certain number of messages sent/ + /// received or after a certain amount of time [_timeSinceUpdate] without an update Future addEventsListener() async { final Client client = _pangeaController.matrixState.client; // if analytics haven't been updated in the last day, update them DateTime? lastUpdated = await _pangeaController.analytics .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); - final DateTime yesterday = DateTime.now().subtract(const Duration(days: 1)); + final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate); if (lastUpdated?.isBefore(yesterday) ?? true) { debugPrint("analytics out-of-date, updating"); await updateAnalytics(); @@ -53,9 +56,9 @@ class MyAnalyticsController extends BaseController { }); } - // given an update from sync stream, check if the update contains - // messages for which analytics will be saved. If so, reset the timer - // and add the event ID to the cache of un-added event IDs + /// given an update from sync stream, check if the update contains + /// messages for which analytics will be saved. If so, reset the timer + /// and add the event ID to the cache of un-added event IDs void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) { for (final entry in update.rooms!.join!.entries) { final Room room = @@ -160,6 +163,7 @@ class MyAnalyticsController extends BaseController { _updateCompleter = Completer(); try { await _updateAnalytics(); + clearMessagesSinceUpdate(); } catch (err, s) { ErrorHandler.logError( e: err, @@ -172,6 +176,9 @@ class MyAnalyticsController extends BaseController { } } + // top level analytics sending function. Send analytics + // for each type of analytics event + // to each of the applicable analytics rooms Future _updateAnalytics() async { // if the user's l2 is not sent, don't send analytics final String? userL2 = _pangeaController.languageController.activeL2Code(); @@ -179,11 +186,6 @@ class MyAnalyticsController extends BaseController { return; } - // top level analytics sending function. Send analytics - // for each type of analytics event - // to each of the applicable analytics rooms - clearMessagesSinceUpdate(); - // fetch a list of all the chats that the user is studying // and a list of all the spaces in which the user is studying await setStudentChats(); @@ -199,9 +201,21 @@ class MyAnalyticsController extends BaseController { .where((lastUpdate) => lastUpdate != null) .cast() .toList(); - lastUpdates.sort((a, b) => a.compareTo(b)); - final DateTime? leastRecentUpdate = - lastUpdates.isNotEmpty ? lastUpdates.first : null; + + /// Get the last time that analytics to for current target language + /// were updated. This my present a problem is the user has analytics + /// rooms for multiple languages, and a non-target language was updated + /// less recently than the target language. In this case, some data may + /// be missing, but a case like that seems relatively rare, and could + /// result in unnecessaily going too far back in the chat history + DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2]; + if (l2AnalyticsLastUpdated == null) { + /// if the target language has never been updated, use the least + /// recent update time + lastUpdates.sort((a, b) => a.compareTo(b)); + l2AnalyticsLastUpdated = + lastUpdates.isNotEmpty ? lastUpdates.first : null; + } // for each chat the user is studying in, get all the messages // since the least recent update analytics update, and sort them @@ -209,7 +223,7 @@ class MyAnalyticsController extends BaseController { final Map> langCodeToMsgs = await getLangCodesToMsgs( userL2, - leastRecentUpdate, + l2AnalyticsLastUpdated, ); final List langCodes = langCodeToMsgs.keys.toList(); @@ -223,7 +237,7 @@ class MyAnalyticsController extends BaseController { // message in this language at the time of the last analytics update // so fallback to the least recent update time final DateTime? lastUpdated = - lastUpdatedMap[analyticsRoom.id] ?? leastRecentUpdate; + lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; // get the corresponding list of recent messages for this langCode final List recentMsgs = diff --git a/lib/pangea/enum/assistance_state_enum.dart b/lib/pangea/enum/assistance_state_enum.dart new file mode 100644 index 000000000..6d3a853da --- /dev/null +++ b/lib/pangea/enum/assistance_state_enum.dart @@ -0,0 +1,43 @@ +// assistance state is, user has not typed a message, user has typed a message and IGC has not run, +// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/constants/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum AssistanceState { + noMessage, + notFetched, + fetching, + fetched, + complete, +} + +extension AssistanceStateExtension on AssistanceState { + Color stateColor(context) { + switch (this) { + case AssistanceState.noMessage: + case AssistanceState.notFetched: + case AssistanceState.fetching: + return Theme.of(context).colorScheme.primary; + case AssistanceState.fetched: + return PangeaColors.igcError; + case AssistanceState.complete: + return AppConfig.success; + } + } + + String tooltip(L10n l10n) { + switch (this) { + case AssistanceState.noMessage: + case AssistanceState.notFetched: + return l10n.runGrammarCorrection; + case AssistanceState.fetching: + return ""; + case AssistanceState.fetched: + return l10n.grammarCorrectionFailed; + case AssistanceState.complete: + return l10n.grammarCorrectionComplete; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 6219c62d2..b8dae7ab3 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -4,14 +4,17 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; @@ -129,6 +132,9 @@ extension PangeaRoom on Room { Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent; + Future> targetLanguages() async => + await _targetLanguages(); + // events Future leaveIfFull() async => await _leaveIfFull(); diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index 00efb6773..5799631b1 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -92,6 +92,34 @@ extension SpaceRoomExtension on Room { return null; } + Future> _targetLanguages() async { + await requestParticipants(); + final students = _students; + + final Map langCounts = {}; + final List allRooms = client.rooms; + for (final User student in students) { + for (final Room room in allRooms) { + if (!room.isAnalyticsRoomOfUser(student.id)) continue; + final String? langCode = room.madeForLang; + if (langCode == null || + langCode.isEmpty || + langCode == LanguageKeys.unknownLanguage) { + continue; + } + final LanguageModel lang = PangeaLanguage.byLangCode(langCode); + langCounts[lang] ??= 0; + langCounts[lang] = langCounts[lang]! + 1; + } + } + // get a list of language models, sorted + // by the number of students who are learning that language + return langCounts.entries.map((entry) => entry.key).toList() + ..sort( + (a, b) => langCounts[b]!.compareTo(langCounts[a]!), + ); + } + // DateTime? get _languageSettingsUpdatedAt { // if (!isSpace) return null; // return languageSettingsStateEvent?.originServerTs ?? creationTime; diff --git a/lib/pangea/pages/analytics/analytics_language_button.dart b/lib/pangea/pages/analytics/analytics_language_button.dart index d74e07be1..2c3923fb4 100644 --- a/lib/pangea/pages/analytics/analytics_language_button.dart +++ b/lib/pangea/pages/analytics/analytics_language_button.dart @@ -16,7 +16,6 @@ class AnalyticsLanguageButton extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton( - icon: const Icon(Icons.language_outlined), tooltip: L10n.of(context)!.changeAnalyticsLanguage, initialValue: value, onSelected: (LanguageModel? lang) { @@ -33,6 +32,21 @@ class AnalyticsLanguageButton extends StatelessWidget { child: Text(lang.getDisplayName(context) ?? lang.langCode), ); }).toList(), + child: TextButton.icon( + label: Text( + L10n.of(context)!.languageButtonLabel( + value.getDisplayName(context) ?? value.langCode, + ), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + Icons.language_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: null, + ), ); } } diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index f62e5f6b5..146a5ac70 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -25,8 +25,9 @@ class BaseAnalyticsPage extends StatefulWidget { final AnalyticsSelected defaultSelected; final AnalyticsSelected? alwaysSelected; final StudentAnalyticsController? myAnalyticsController; + final List targetLanguages; - const BaseAnalyticsPage({ + BaseAnalyticsPage({ super.key, required this.pageTitle, required this.tabs, @@ -34,7 +35,10 @@ class BaseAnalyticsPage extends StatefulWidget { required this.defaultSelected, this.selectedView, this.myAnalyticsController, - }); + targetLanguages, + }) : targetLanguages = (targetLanguages?.isNotEmpty ?? false) + ? targetLanguages + : MatrixState.pangeaController.pLanguageStore.targetOptions; @override State createState() => BaseAnalyticsController(); @@ -159,7 +163,7 @@ class BaseAnalyticsController extends State { } Future toggleSpaceLang(LanguageModel lang) async { - await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang); + await pangeaController.analytics.setCurrentAnalyticsLang(lang); await setChartData(); refreshStream.add(false); } diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 1c0445d5a..3d70c9b4c 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -108,29 +108,26 @@ class BaseAnalyticsView extends StatelessWidget { ? Column( children: [ Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.student) - IconButton( - icon: const Icon(Icons.refresh), - onPressed: controller.onRefresh, - tooltip: L10n.of(context)!.refresh, - ), + // if (controller.widget.defaultSelected.type == + // AnalyticsEntryType.student) + // IconButton( + // icon: const Icon(Icons.refresh), + // onPressed: controller.onRefresh, + // tooltip: L10n.of(context)!.refresh, + // ), TimeSpanMenuButton( value: controller.currentTimeSpan, onChange: (TimeSpan value) => controller.toggleTimeSpan(context, value), ), - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) - AnalyticsLanguageButton( - value: controller.pangeaController.analytics - .currentAnalyticsSpaceLang, - onChange: (lang) => controller.toggleSpaceLang(lang), - languages: controller - .pangeaController.pLanguageStore.targetOptions, - ), + AnalyticsLanguageButton( + value: controller + .pangeaController.analytics.currentAnalyticsLang, + onChange: (lang) => controller.toggleSpaceLang(lang), + languages: controller.widget.targetLanguages, + ), ], ), Expanded( diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index e169922ca..8651b7a74 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -355,15 +355,17 @@ class ConstructMessagesDialog extends StatelessWidget { final msgEventMatches = controller.getMessageEventMatches(); + final noData = controller.constructs![controller.lemmaIndex].uses.length > + controller._msgEvents.length; + return AlertDialog( title: Center(child: Text(controller.widget.controller.currentLemma!)), content: SizedBox( - height: 350, - width: 500, + height: noData ? 90 : 250, + width: noData ? 200 : 400, child: Column( children: [ - if (controller.constructs![controller.lemmaIndex].uses.length > - controller._msgEvents.length) + if (noData) Center( child: Padding( padding: const EdgeInsets.all(8.0), @@ -398,8 +400,8 @@ class ConstructMessagesDialog extends StatelessWidget { child: Text( L10n.of(context)!.close.toUpperCase(), style: TextStyle( - color: - Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150), + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, ), ), ), diff --git a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart index 64875bfba..b32780761 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; @@ -33,6 +34,18 @@ class SpaceAnalyticsV2Controller extends State { List students = []; String? get spaceId => GoRouterState.of(context).pathParameters['spaceid']; Room? _spaceRoom; + List targetLanguages = []; + + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () async { + if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) { + context.go('/rooms'); + } + getChatAndStudents(); + }); + } Room? get spaceRoom { if (_spaceRoom == null || _spaceRoom!.id != spaceId) { @@ -44,23 +57,11 @@ class SpaceAnalyticsV2Controller extends State { context.go('/rooms/analytics'); return null; } - getChatAndStudents(); + getChatAndStudents().then((_) => setTargetLanguages()); } return _spaceRoom; } - @override - void initState() { - super.initState(); - debugPrint("init space analytics"); - Future.delayed(Duration.zero, () async { - if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) { - context.go('/rooms'); - } - getChatAndStudents(); - }); - } - Future getChatAndStudents() async { try { await spaceRoom?.postLoad(); @@ -97,12 +98,12 @@ class SpaceAnalyticsV2Controller extends State { } } - // @override - // void dispose() { - // super.dispose(); - // refreshTimer?.cancel(); - // stateSub?.cancel(); - // } + Future setTargetLanguages() async { + // get a list of language models, sorted by the + // number of students who are learning that language + targetLanguages = await spaceRoom?.targetLanguages() ?? []; + setState(() {}); + } @override Widget build(BuildContext context) { diff --git a/lib/pangea/pages/analytics/space_analytics/space_analytics_view.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics_view.dart index 5e0008555..c72ec3c26 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics_view.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics_view.dart @@ -59,6 +59,7 @@ class SpaceAnalyticsView extends StatelessWidget { AnalyticsEntryType.space, controller.spaceRoom?.name ?? "", ), + targetLanguages: controller.targetLanguages, ) : const SizedBox(); } diff --git a/lib/pangea/pages/analytics/space_list/space_list.dart b/lib/pangea/pages/analytics/space_list/space_list.dart index 058d54e63..e65bb6152 100644 --- a/lib/pangea/pages/analytics/space_list/space_list.dart +++ b/lib/pangea/pages/analytics/space_list/space_list.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart'; import 'package:flutter/material.dart'; @@ -22,26 +23,58 @@ class AnalyticsSpaceList extends StatefulWidget { class AnalyticsSpaceListController extends State { PangeaController pangeaController = MatrixState.pangeaController; List spaces = []; + StreamSubscription? stateSub; + List targetLanguages = []; @override void initState() { super.initState(); - Matrix.of(context).client.spacesImTeaching.then((spaceList) { - spaceList = spaceList - .where( - (space) => !spaceList.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); - spaces = spaceList; - setState(() {}); - }); + setSpaceList().then((_) => setTargetLanguages()); + + // reload dropdowns when their values change in analytics page + stateSub = pangeaController.analytics.stateStream.listen( + (_) => setState(() {}), + ); + } + + @override + void dispose() { + stateSub?.cancel(); + super.dispose(); } StreamController refreshStream = StreamController.broadcast(); + Future setSpaceList() async { + final spaceList = await Matrix.of(context).client.spacesImTeaching; + spaces = spaceList + .where( + (space) => !spaceList.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ), + ) + .toList(); + setState(() {}); + } + + Future setTargetLanguages() async { + if (spaces.isEmpty) return; + final Map langCounts = {}; + for (final Room space in spaces) { + final List targetLangs = await space.targetLanguages(); + for (final LanguageModel lang in targetLangs) { + langCounts[lang] ??= 0; + langCounts[lang] = langCounts[lang]! + 1; + } + } + targetLanguages = langCounts.entries.map((entry) => entry.key).toList() + ..sort( + (a, b) => langCounts[b]!.compareTo(langCounts[a]!), + ); + setState(() {}); + } + void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) { pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan); refreshStream.add(false); @@ -49,7 +82,7 @@ class AnalyticsSpaceListController extends State { } Future toggleSpaceLang(LanguageModel lang) async { - await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang); + await pangeaController.analytics.setCurrentAnalyticsLang(lang); refreshStream.add(false); setState(() {}); } diff --git a/lib/pangea/pages/analytics/space_list/space_list_view.dart b/lib/pangea/pages/analytics/space_list/space_list_view.dart index 5f5bf22da..7ef5fb45e 100644 --- a/lib/pangea/pages/analytics/space_list/space_list_view.dart +++ b/lib/pangea/pages/analytics/space_list/space_list_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart'; @@ -5,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import '../../../enum/time_span.dart'; import '../base_analytics.dart'; import 'space_list.dart'; @@ -32,25 +32,29 @@ class AnalyticsSpaceListView extends StatelessWidget { icon: const Icon(Icons.close_outlined), onPressed: () => context.pop(), ), - actions: [ - TimeSpanMenuButton( - value: - controller.pangeaController.analytics.currentAnalyticsTimeSpan, - onChange: (TimeSpan value) => controller.toggleTimeSpan( - context, - value, - ), - ), - AnalyticsLanguageButton( - value: - controller.pangeaController.analytics.currentAnalyticsSpaceLang, - onChange: (lang) => controller.toggleSpaceLang(lang), - languages: controller.pangeaController.pLanguageStore.targetOptions, - ), - ], ), body: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TimeSpanMenuButton( + value: controller + .pangeaController.analytics.currentAnalyticsTimeSpan, + onChange: (TimeSpan value) => controller.toggleTimeSpan( + context, + value, + ), + ), + AnalyticsLanguageButton( + value: + controller.pangeaController.analytics.currentAnalyticsLang, + onChange: (lang) => controller.toggleSpaceLang(lang), + languages: + controller.pangeaController.pLanguageStore.targetOptions, + ), + ], + ), Flexible( child: ListView.builder( itemCount: controller.spaces.length, diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index 65bd533e8..d6bc9d766 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,7 +1,12 @@ import 'dart:async'; import 'dart:developer'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; +import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -75,6 +80,24 @@ class StudentAnalyticsController extends State { return id; } + List get targetLanguages { + final LanguageModel? l2 = + _pangeaController.languageController.activeL2Model(); + final List analyticsRoomLangs = + _pangeaController.matrixState.client.allMyAnalyticsRooms + .map((analyticsRoom) => analyticsRoom.madeForLang) + .where((langCode) => langCode != null) + .map((langCode) => PangeaLanguage.byLangCode(langCode!)) + .where( + (langModel) => langModel.langCode != LanguageKeys.unknownLanguage, + ) + .toList(); + if (l2 != null) { + analyticsRoomLangs.add(l2); + } + return analyticsRoomLangs.toSet().toList(); + } + @override Widget build(BuildContext context) { return PLoadingStatusV2( diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart index 5b8924581..6ea754891 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart @@ -59,6 +59,7 @@ class StudentAnalyticsView extends StatelessWidget { AnalyticsEntryType.student, L10n.of(context)!.allChatsAndClasses, ), + targetLanguages: controller.targetLanguages, ) : const SizedBox(); } diff --git a/lib/pangea/pages/analytics/time_span_menu_button.dart b/lib/pangea/pages/analytics/time_span_menu_button.dart index 23d2ad0c8..32f6668bc 100644 --- a/lib/pangea/pages/analytics/time_span_menu_button.dart +++ b/lib/pangea/pages/analytics/time_span_menu_button.dart @@ -15,7 +15,6 @@ class TimeSpanMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton( - icon: const Icon(Icons.calendar_month_outlined), tooltip: L10n.of(context)!.changeDateRange, initialValue: value, onSelected: (TimeSpan? timeSpan) { @@ -32,6 +31,19 @@ class TimeSpanMenuButton extends StatelessWidget { child: Text(timeSpan.string(context)), ); }).toList(), + child: TextButton.icon( + label: Text( + value.string(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + Icons.calendar_month_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: null, + ), ); } } diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart index 4fdb38604..ff7d0068a 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart @@ -17,6 +17,7 @@ class ClassDescriptionButton extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + final ScrollController scrollController = ScrollController(); return Column( children: [ ListTile( @@ -26,14 +27,27 @@ class ClassDescriptionButton extends StatelessWidget { foregroundColor: iconColor, child: const Icon(Icons.topic_outlined), ), - subtitle: Text( - room.topic.isEmpty - ? (room.isRoomAdmin - ? (room.isSpace - ? L10n.of(context)!.classDescriptionDesc - : L10n.of(context)!.chatTopicDesc) - : L10n.of(context)!.topicNotSet) - : room.topic, + subtitle: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 190, + ), + child: Scrollbar( + controller: scrollController, + interactive: true, + child: SingleChildScrollView( + controller: scrollController, + primary: false, + child: Text( + room.topic.isEmpty + ? (room.isRoomAdmin + ? (room.isSpace + ? L10n.of(context)!.classDescriptionDesc + : L10n.of(context)!.chatTopicDesc) + : L10n.of(context)!.topicNotSet) + : room.topic, + ), + ), + ), ), title: Text( room.isSpace diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index bd09c7131..9705a9ca1 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -4,7 +4,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; class PangeaAnyState { final Map _layerLinkAndKeys = {}; - OverlayEntry? overlay; + List entries = []; dispose() { closeOverlay(); @@ -32,26 +32,32 @@ class PangeaAnyState { _layerLinkAndKeys.remove(transformTargetId); } - void openOverlay(OverlayEntry entry, BuildContext context) { - closeOverlay(); - overlay = entry; - Overlay.of(context).insert(overlay!); + void openOverlay( + OverlayEntry entry, + BuildContext context, { + bool closePrevOverlay = true, + }) { + if (closePrevOverlay) { + closeOverlay(); + } + entries.add(entry); + Overlay.of(context).insert(entry); } void closeOverlay() { - if (overlay != null) { + if (entries.isNotEmpty) { try { - overlay?.remove(); + entries.last.remove(); } catch (err, s) { ErrorHandler.logError( e: err, s: s, data: { - "overlay": overlay, + "overlay": entries.last, }, ); } - overlay = null; + entries.removeLast(); } } diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 43350f518..b9eecd799 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -94,6 +94,7 @@ class InstructionsController { ), cardSize: const Size(300.0, 300.0), transformTargetId: transformTargetKey, + closePrevOverlay: false, ), ); } diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 84e9b9a2a..ce8c63d99 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -25,9 +25,12 @@ class OverlayUtil { Color? backgroundColor, Alignment? targetAnchor, Alignment? followerAnchor, + bool closePrevOverlay = true, }) { try { - MatrixState.pAnyState.closeOverlay(); + if (closePrevOverlay) { + MatrixState.pAnyState.closeOverlay(); + } final LayerLinkAndKey layerLinkAndKey = MatrixState.pAnyState.layerLinkAndKey(transformTargetId); @@ -58,7 +61,8 @@ class OverlayUtil { ), ); - MatrixState.pAnyState.openOverlay(entry, context); + MatrixState.pAnyState + .openOverlay(entry, context, closePrevOverlay: closePrevOverlay); } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); @@ -72,6 +76,7 @@ class OverlayUtil { required String transformTargetId, backDropToDismiss = true, Color? borderColor, + bool closePrevOverlay = true, }) { try { final LayerLinkAndKey layerLinkAndKey = @@ -105,6 +110,7 @@ class OverlayUtil { offset: cardOffset, backDropToDismiss: backDropToDismiss, borderColor: borderColor, + closePrevOverlay: closePrevOverlay, ); } catch (err, stack) { debugger(when: kDebugMode); @@ -180,7 +186,7 @@ class OverlayUtil { return Offset(dx, dy); } - static bool get isOverlayOpen => MatrixState.pAnyState.overlay != null; + static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty; } class TransparentBackdrop extends StatelessWidget { diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 434128b1e..2468d6b96 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -136,8 +136,8 @@ class ToolbarDisplayController { backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), ); - if (MatrixState.pAnyState.overlay != null) { - overlayId = MatrixState.pAnyState.overlay.hashCode.toString(); + if (MatrixState.pAnyState.entries.isNotEmpty) { + overlayId = MatrixState.pAnyState.entries.last.hashCode.toString(); } if (mode != null) { @@ -151,8 +151,11 @@ class ToolbarDisplayController { bool get highlighted { if (overlayId == null) return false; - if (MatrixState.pAnyState.overlay == null) overlayId = null; - return MatrixState.pAnyState.overlay.hashCode.toString() == overlayId; + if (MatrixState.pAnyState.entries.isEmpty) { + overlayId = null; + return false; + } + return MatrixState.pAnyState.entries.last.hashCode.toString() == overlayId; } } diff --git a/lib/pangea/widgets/subscription/subscription_options.dart b/lib/pangea/widgets/subscription/subscription_options.dart index 41c8cbe4f..d40f68023 100644 --- a/lib/pangea/widgets/subscription/subscription_options.dart +++ b/lib/pangea/widgets/subscription/subscription_options.dart @@ -109,7 +109,7 @@ class SubscriptionCard extends StatelessWidget { title ?? subscription?.displayName(context) ?? '', textAlign: TextAlign.center, style: TextStyle( - fontSize: 24, + fontSize: 20, color: enabled ? null : const Color.fromARGB(255, 174, 174, 174), ), diff --git a/needed-translations.txt b/needed-translations.txt index ec54ac359..812672548 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -864,7 +864,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "be": [ @@ -2365,7 +2366,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "bn": [ @@ -3862,7 +3864,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "bo": [ @@ -5363,7 +5366,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ca": [ @@ -6266,7 +6270,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "cs": [ @@ -7251,7 +7256,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "de": [ @@ -8119,7 +8125,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "el": [ @@ -9571,7 +9578,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "eo": [ @@ -10721,7 +10729,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "es": [ @@ -10737,7 +10746,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "et": [ @@ -11605,7 +11615,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "eu": [ @@ -12475,7 +12486,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "fa": [ @@ -13482,7 +13494,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "fi": [ @@ -14453,7 +14466,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "fil": [ @@ -15780,7 +15794,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "fr": [ @@ -16786,7 +16801,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ga": [ @@ -17921,7 +17937,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "gl": [ @@ -18789,7 +18806,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "he": [ @@ -20043,7 +20061,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "hi": [ @@ -21537,7 +21556,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "hr": [ @@ -22484,7 +22504,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "hu": [ @@ -23368,7 +23389,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ia": [ @@ -24855,7 +24877,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "id": [ @@ -25729,7 +25752,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ie": [ @@ -26987,7 +27011,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "it": [ @@ -27912,7 +27937,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ja": [ @@ -28948,7 +28974,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ka": [ @@ -30303,7 +30330,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ko": [ @@ -31173,7 +31201,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "lt": [ @@ -32209,7 +32238,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "lv": [ @@ -33085,7 +33115,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "nb": [ @@ -34285,7 +34316,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "nl": [ @@ -35249,7 +35281,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "pl": [ @@ -36222,7 +36255,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "pt": [ @@ -37701,7 +37735,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "pt_BR": [ @@ -38575,7 +38610,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "pt_PT": [ @@ -39776,7 +39812,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ro": [ @@ -40784,7 +40821,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ru": [ @@ -41658,7 +41696,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "sk": [ @@ -42925,7 +42964,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "sl": [ @@ -44322,7 +44362,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "sr": [ @@ -45493,7 +45534,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "sv": [ @@ -46398,7 +46440,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "ta": [ @@ -47896,7 +47939,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "th": [ @@ -49348,7 +49392,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "tr": [ @@ -50216,7 +50261,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "uk": [ @@ -51121,7 +51167,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "vi": [ @@ -52474,7 +52521,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "zh": [ @@ -53342,7 +53390,8 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ], "zh_Hant": [ @@ -54491,6 +54540,7 @@ "practice", "noLanguagesSet", "noActivitiesFound", - "previous" + "previous", + "languageButtonLabel" ] }