diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index d610de1dd..53efbf38c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4943,5 +4943,16 @@ "launchActivityToChats": "Launch activity to chats", "searchChats": "Search chats", "selectChats": "Select chats", - "selectChatToStart": "Complete! Select a chat to start" + "selectChatToStart": "Complete! Select a chat to start", + "configureSpace": "Configure space", + "pinMessages": "Pin messages", + "setJoinRules": "Set join rules", + "displayNavigationRail": "Show navigation rail on mobile", + "changeGeneralSettings": "Change general settings", + "inviteOtherUsersToRoom": "Invite other users", + "changeTheNameOfTheSpace": "Change the name of the space", + "changeTheDescription": "Change the description", + "changeThePermissions": "Change the permissions", + "introductions": "Introductions", + "announcements": "Announcements" } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 33a97c5cc..d04d20be0 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -134,6 +134,9 @@ abstract class AppConfig { static bool swipeRightToLeftToReply = true; static bool? sendOnEnter; static bool showPresences = true; + // #Pangea + static bool displayNavigationRail = true; + // Pangea# static bool experimentalVoip = false; static const bool hideTypingUsernames = false; static const bool hideAllStateEvents = false; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index d014a23a7..e1d6bd72a 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -33,7 +33,6 @@ import 'package:fluffychat/pangea/activity_generator/activity_generator.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; -import 'package:fluffychat/pangea/layouts/bottom_nav_layout.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart'; import 'package:fluffychat/pangea/login/pages/signup.dart'; @@ -210,15 +209,7 @@ abstract class AppRoutes { ), sideView: child, ) - // #Pangea - // : child, - : FluffyThemes.isColumnMode(context) || - (state.fullPath?.split("/").reversed.elementAt(1) == - 'rooms' && - state.pathParameters['roomid'] != null) - ? child - : BottomNavLayout(mainView: child), - // Pangea# + : child, ), routes: [ GoRoute( @@ -352,6 +343,39 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), + // #Pangea + GoRoute( + path: 'homepage', + redirect: loggedOutRedirect, + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const SuggestionsPage(), + ), + routes: [ + ...newRoomRoutes, + GoRoute( + path: '/planner', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const ActivityPlannerPage(), + ), + redirect: loggedOutRedirect, + routes: [ + GoRoute( + path: '/generator', + redirect: loggedOutRedirect, + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const ActivityGenerator(), + ), + ), + ], + ), + ], + ), // Pangea# ShellRoute( pageBuilder: (context, state, child) => defaultPageBuilder( @@ -365,40 +389,6 @@ abstract class AppRoutes { : child, ), routes: [ - // #Pangea - GoRoute( - path: '/homepage', - redirect: loggedOutRedirect, - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SuggestionsPage(), - ), - routes: [ - ...newRoomRoutes, - GoRoute( - path: '/planner', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const ActivityPlannerPage(), - ), - redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: '/generator', - redirect: loggedOutRedirect, - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const ActivityGenerator(), - ), - ), - ], - ), - ], - ), - // Pangea# GoRoute( path: 'settings', pageBuilder: (context, state) => defaultPageBuilder( @@ -802,21 +792,14 @@ abstract class AppRoutes { ? TwoColumnLayout( mainView: ChatList( activeChat: state.pathParameters['roomid'], - // #Pangea activeSpaceId: state.uri.queryParameters['spaceId'], activeFilter: state.uri.queryParameters['filter'], - // Pangea# displayNavigationRail: state.path?.startsWith('/rooms/settings') != true, ), sideView: child, ) - : FluffyThemes.isColumnMode(context) || - (state.fullPath?.split("/").reversed.elementAt(1) == - 'rooms' && - state.pathParameters['roomid'] != null) - ? child - : BottomNavLayout(mainView: child), + : child, ); // Pangea# } diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 9e82c6b38..8b7bae372 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -32,6 +32,10 @@ abstract class SettingKeys { 'chat.fluffy.swipeRightToLeftToReply'; static const String experimentalVoip = 'chat.fluffy.experimental_voip'; static const String showPresences = 'chat.fluffy.show_presences'; + // #Pangea + static const String displayNavigationRail = + 'chat.fluffy.display_navigation_rail'; + // Pangea# } enum AppSettings { diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 39e08dab3..ec4280601 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -201,7 +201,8 @@ class ChatListViewBody extends StatelessWidget { // #Pangea // if (spaceDelegateCandidates.isNotEmpty && // !controller.widget.displayNavigationRail) - if (!controller.widget.displayNavigationRail) + if (!AppConfig.displayNavigationRail && + !FluffyThemes.isColumnMode(context)) // Pangea# ActiveFilter.spaces, ] diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 35d5c07dc..f69d52840 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/chat_list/widgets/chat_list_view_body_wrapper.dart'; @@ -31,8 +32,12 @@ class ChatListView extends StatelessWidget { }, child: Row( children: [ - if (FluffyThemes.isColumnMode(context) && - controller.widget.displayNavigationRail) ...[ + // #Pangea + // if (FluffyThemes.isColumnMode(context) && + // controller.widget.displayNavigationRail) ...[ + if (FluffyThemes.isColumnMode(context) || + AppConfig.displayNavigationRail) ...[ + // Pangea# SpacesNavigationRail( activeSpaceId: controller.activeSpaceId, onGoToChats: controller.clearActiveSpace, diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index 0e844e05d..6b4d3c540 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -44,7 +44,14 @@ class NaviRailItem extends StatelessWidget { bottom: 8, left: 0, child: AnimatedContainer( - width: isSelected ? 8 : 0, + // #Pangea + // width: isSelected ? 8 : 0, + width: isSelected + ? FluffyThemes.isColumnMode(context) + ? 8 + : 4 + : 0, + // Pangea# duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, decoration: BoxDecoration( diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 24533cc31..879a57213 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -527,6 +527,18 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound; + + // #Pangea + final joinedParents = room?.spaceParents + .map((parent) { + final roomId = parent.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .toList(); + // Pangea# + return Scaffold( // #Pangea // appBar: AppBar( @@ -539,14 +551,51 @@ class _SpaceViewState extends State { _onSpaceAction(SpaceActions.settings); }, child: AppBar( - // Pangea# - leading: FluffyThemes.isColumnMode(context) - ? null + // leading: FluffyThemes.isColumnMode(context) + // ? null + // : Center( + // child: CloseButton( + // onPressed: widget.onBack, + // ), + // ), + leading: joinedParents?.isEmpty ?? true + ? FluffyThemes.isColumnMode(context) + ? null + : Center( + child: CloseButton( + onPressed: widget.onBack, + ), + ) : Center( - child: CloseButton( - onPressed: widget.onBack, - ), + child: joinedParents!.length == 1 + ? IconButton( + icon: const Icon(Icons.arrow_back_outlined), + onPressed: () => + widget.toParentSpace(joinedParents.first.id), + ) + : PopupMenuButton( + popUpAnimationStyle: AnimationStyle( + duration: const Duration(milliseconds: 0), + ), + tooltip: null, + useRootNavigator: true, + icon: const Icon(Icons.arrow_back_outlined), + itemBuilder: (context) { + return [ + ...joinedParents.mapIndexed((i, room) { + return PopupMenuItem( + value: i, + child: Text(room.getLocalizedDisplayname()), + ); + }), + ]; + }, + onSelected: (i) { + widget.toParentSpace(joinedParents[i].id); + }, + ), ), + // Pangea# automaticallyImplyLeading: false, titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0, title: ListTile( @@ -660,14 +709,16 @@ class _SpaceViewState extends State { // Pangea# .toList(); - final joinedParents = room.spaceParents - .map((parent) { - final roomId = parent.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .toList(); + // #Pangea + // final joinedParents = room.spaceParents + // .map((parent) { + // final roomId = parent.roomId; + // if (roomId == null) return null; + // return room.client.getRoomById(roomId); + // }) + // .whereType() + // .toList(); + // Pangea# final filter = _filterController.text.trim().toLowerCase(); return CustomScrollView( slivers: [ @@ -715,51 +766,51 @@ class _SpaceViewState extends State { ), ), ), - SliverList.builder( - itemCount: joinedParents.length, - itemBuilder: (context, i) { - final displayname = - joinedParents[i].getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - minVerticalPadding: 0, - leading: Icon( - Icons.adaptive.arrow_back_outlined, - size: 16, - ), - title: Row( - children: [ - Avatar( - mxContent: joinedParents[i].avatar, - name: displayname, - // #Pangea - userId: joinedParents[i].directChatMatrixID, - // Pangea# - size: Avatar.defaultSize / 2, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - ), - const SizedBox(width: 8), - Expanded(child: Text(displayname)), - ], - ), - onTap: () => - widget.toParentSpace(joinedParents[i].id), - ), - ), - ); - }, - ), // #Pangea + // SliverList.builder( + // itemCount: joinedParents.length, + // itemBuilder: (context, i) { + // final displayname = + // joinedParents[i].getLocalizedDisplayname(); + // return Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 8, + // vertical: 1, + // ), + // child: Material( + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // clipBehavior: Clip.hardEdge, + // child: ListTile( + // minVerticalPadding: 0, + // leading: Icon( + // Icons.adaptive.arrow_back_outlined, + // size: 16, + // ), + // title: Row( + // children: [ + // Avatar( + // mxContent: joinedParents[i].avatar, + // name: displayname, + // // #Pangea + // userId: joinedParents[i].directChatMatrixID, + // // Pangea# + // size: Avatar.defaultSize / 2, + // borderRadius: BorderRadius.circular( + // AppConfig.borderRadius / 4, + // ), + // ), + // const SizedBox(width: 8), + // Expanded(child: Text(displayname)), + // ], + // ), + // onTap: () => + // widget.toParentSpace(joinedParents[i].id), + // ), + // ), + // ); + // }, + // ), KnockingUsersIndicator(room: room), // Pangea# SliverList.builder( 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 e0f77ffaa..f4e25d9f9 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -20,7 +20,10 @@ class ChatPermissionsSettingsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context).chatPermissions), + // #Pangea + // title: Text(L10n.of(context).chatPermissions), + title: Text(L10n.of(context).permissions), + // Pangea# ), body: MaxWidthBody( child: StreamBuilder( @@ -36,28 +39,11 @@ class ChatPermissionsSettingsView extends StatelessWidget { final powerLevelsContent = Map.from( room.getState(EventTypes.RoomPowerLevels)?.content ?? {}, ); - final powerLevels = - Map.from(powerLevelsContent) // #Pangea - // ..removeWhere((k, v) => v is! int); - ..removeWhere( - (k, v) => - v is! int || - k.equals("m.call.invite") || - k.equals("historical") || - k.equals("state_default"), - ); - // Pangea# + final powerLevels = Map.from(powerLevelsContent) + ..removeWhere((k, v) => v is! int); final eventsPowerLevels = Map.from( powerLevelsContent.tryGetMap('events') ?? {}, - // #Pangea - )..removeWhere( - (k, v) => - v is! int || - k.equals("pangea.usranalytics") || - k.equals(EventTypes.RoomPowerLevels), - ); - // )..removeWhere((k, v) => v is! int); - // Pangea# + )..removeWhere((k, v) => v is! int); return Column( children: [ ListTile( @@ -69,7 +55,10 @@ class ChatPermissionsSettingsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context).chatPermissions, + // #Pangea + // L10n.of(context).chatPermissions, + L10n.of(context).permissions, + // Pangea# style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, @@ -90,48 +79,57 @@ class ChatPermissionsSettingsView extends StatelessWidget { newLevel: level, ), canEdit: room.canChangePowerLevel, + // #Pangea + room: room, + // Pangea# ), - // #Pangea - // Divider(color: theme.dividerColor), - // ListTile( - // title: Text( - // L10n.of(context).notifications, - // style: TextStyle( - // color: theme.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# Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context).configureChat, + L10n.of(context).notifications, + style: TextStyle( + color: theme.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 + room: room, + // Pangea# + ); + }, + ), + Divider(color: theme.dividerColor), + ListTile( + title: Text( + // #Pangea + // L10n.of(context).configureChat, + room.isSpace + ? L10n.of(context).configureSpace + : L10n.of(context).configureChat, + // Pangea# style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, @@ -151,6 +149,9 @@ class ChatPermissionsSettingsView extends StatelessWidget { newLevel: level, category: 'events', ), + // #Pangea + room: room, + // Pangea# ), ], ), diff --git a/lib/pages/chat_permissions_settings/permission_list_tile.dart b/lib/pages/chat_permissions_settings/permission_list_tile.dart index 704bdc696..a8f14e002 100644 --- a/lib/pages/chat_permissions_settings/permission_list_tile.dart +++ b/lib/pages/chat_permissions_settings/permission_list_tile.dart @@ -11,6 +11,9 @@ class PermissionsListTile extends StatelessWidget { final String? category; final void Function(int? level)? onChanged; final bool canEdit; + // #Pangea + final Room room; + // Pangea# const PermissionsListTile({ super.key, @@ -19,6 +22,9 @@ class PermissionsListTile extends StatelessWidget { this.category, required this.onChanged, required this.canEdit, + // #Pangea + required this.room, + // Pangea# }); String getLocalizedPowerLevelString(BuildContext context) { @@ -29,15 +35,27 @@ class PermissionsListTile extends StatelessWidget { case 'events_default': return L10n.of(context).sendMessages; case 'state_default': - return L10n.of(context).changeGeneralChatSettings; + // #Pangea + // return L10n.of(context).changeGeneralChatSettings; + return L10n.of(context).changeGeneralSettings; + // Pangea# case 'ban': - return L10n.of(context).banFromChat; + // #Pangea + // return L10n.of(context).banFromChat; + return L10n.of(context).ban; + // Pangea# case 'kick': - return L10n.of(context).kickFromChat; + // #Pangea + // return L10n.of(context).kickFromChat; + return L10n.of(context).kick; + // Pangea# case 'redact': return L10n.of(context).deleteMessage; case 'invite': - return L10n.of(context).inviteOtherUsers; + // #Pangea + // return L10n.of(context).inviteOtherUsers; + return L10n.of(context).inviteOtherUsersToRoom; + // Pangea# } } else if (category == 'notifications') { switch (permissionKey) { @@ -49,12 +67,20 @@ class PermissionsListTile extends StatelessWidget { case EventTypes.RoomName: // #Pangea // return L10n.of(context).changeTheNameOfTheGroup; - return L10n.of(context).changeTheNameOfTheChat; + return room.isSpace + ? L10n.of(context).changeTheNameOfTheSpace + : L10n.of(context).changeTheNameOfTheChat; // Pangea# case EventTypes.RoomTopic: - return L10n.of(context).changeTheDescriptionOfTheGroup; + // #Pangea + // return L10n.of(context).changeTheDescriptionOfTheGroup; + return L10n.of(context).changeTheDescription; + // Pangea# case EventTypes.RoomPowerLevels: - return L10n.of(context).changeTheChatPermissions; + // #Pangea + // return L10n.of(context).changeTheChatPermissions; + return L10n.of(context).changeThePermissions; + // Pangea# case EventTypes.HistoryVisibility: return L10n.of(context).changeTheVisibilityOfChatHistory; case EventTypes.RoomCanonicalAlias: @@ -70,6 +96,10 @@ class PermissionsListTile extends StatelessWidget { // #Pangea case EventTypes.SpaceChild: return L10n.of(context).spaceChildPermission; + case EventTypes.RoomPinnedEvents: + return L10n.of(context).pinMessages; + case EventTypes.RoomJoinRules: + return L10n.of(context).setJoinRules; // Pangea# } } diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index bcb397fd7..b2146b55f 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; @@ -188,10 +187,8 @@ class NewGroupController extends State { ); } } - // if a timeout happened, don't redirect to the chat - if (error != null) return; - // Pangea# context.go('/rooms/$roomId/invite?filter=groups'); + // Pangea# } Future _createSpace() async { @@ -220,6 +217,8 @@ class NewGroupController extends State { // context.pop(spaceId); final spaceId = await Matrix.of(context).client.createPangeaSpace( name: nameController.text, + introChatName: L10n.of(context).introductions, + announcementsChatName: L10n.of(context).announcements, visibility: groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, joinRules: @@ -237,8 +236,6 @@ class NewGroupController extends State { GoogleAnalytics.createClass(room.name, spaceCode); } - // if a timeout happened, don't redirect to the space - if (error != null) return; context.go("/rooms?spaceId=$spaceId"); // Pangea# } @@ -273,23 +270,9 @@ class NewGroupController extends State { switch (createGroupType) { case CreateGroupType.group: - // #Pangea - // await _createGroup(); - await _createGroup().timeout( - const Duration( - seconds: AppConfig.roomCreationTimeoutSeconds, - ), - ); - // Pangea# + await _createGroup(); case CreateGroupType.space: - // #Pangea - // await _createSpace(); - await _createSpace().timeout( - const Duration( - seconds: AppConfig.roomCreationTimeoutSeconds, - ), - ); - // Pangea# + await _createSpace(); } } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index a912fd401..4c51ea86d 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -41,7 +41,11 @@ class SettingsView extends StatelessWidget { // Pangea# return Row( children: [ - if (FluffyThemes.isColumnMode(context)) ...[ + // #Pangea + // if (FluffyThemes.isColumnMode(context)) ...[ + if (FluffyThemes.isColumnMode(context) || + AppConfig.displayNavigationRail) ...[ + // Pangea# SpacesNavigationRail( activeSpaceId: null, onGoToChats: () => context.go('/rooms'), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index d234759df..05bde2f42 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -359,6 +359,14 @@ class SettingsStyleView extends StatelessWidget { storeKey: SettingKeys.separateChatTypes, defaultValue: AppConfig.separateChatTypes, ), + // #Pangea + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context).displayNavigationRail, + // onChanged: (b) => AppConfig.displayNavigationRail = b, + // storeKey: SettingKeys.displayNavigationRail, + // defaultValue: AppConfig.displayNavigationRail, + // ), + // Pangea# ], ), ), diff --git a/lib/pangea/activity_planner/activity_planner_builder.dart b/lib/pangea/activity_planner/activity_planner_builder.dart index a45521044..36e72c0c9 100644 --- a/lib/pangea/activity_planner/activity_planner_builder.dart +++ b/lib/pangea/activity_planner/activity_planner_builder.dart @@ -211,7 +211,7 @@ class ActivityPlannerBuilderState extends State { } Future clearEdits() async { - _resetActivity(); + await _resetActivity(); if (mounted) { setState(() { isEditing = false; diff --git a/lib/pangea/activity_suggestions/activity_room_selection.dart b/lib/pangea/activity_suggestions/activity_room_selection.dart index 155363c65..a2523b7d1 100644 --- a/lib/pangea/activity_suggestions/activity_room_selection.dart +++ b/lib/pangea/activity_suggestions/activity_room_selection.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -178,6 +179,13 @@ class ActivityRoomSelectionState extends State { preset: CreateRoomPreset.trustedPrivateChat, initialState: [ BotOptionsModel(mode: BotMode.directChat).toStateEvent, + StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: defaultPowerLevels( + Matrix.of(context).client.userID!, + ), + ), if (avatar != null && avatarUrl != null) StateEvent( type: EventTypes.RoomAvatar, diff --git a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart index c2e16f87d..3a1282b3f 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_dialog.dart @@ -86,320 +86,350 @@ class ActivitySuggestionDialogState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Stack( - alignment: Alignment.center, - children: [ - SizedBox( - width: _width, - child: widget.controller.avatar != null - ? Image.memory( - widget.controller.avatar!, - fit: BoxFit.cover, - ) - : widget.controller.updatedActivity.imageURL != - null - ? widget.controller.updatedActivity - .imageURL! - .startsWith("mxc") - ? MxcImage( - uri: Uri.parse( - widget.controller.updatedActivity - .imageURL!, - ), - width: _width, - height: 200, - cacheKey: widget.controller - .updatedActivity.bookmarkId, - fit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: widget.controller - .updatedActivity.imageURL!, - fit: BoxFit.cover, - placeholder: (context, url) => - const Center( - child: - CircularProgressIndicator(), - ), - errorWidget: - (context, url, error) => - const SizedBox(), - ) - : null, - ), - if (widget.controller.isEditing) - Positioned( - bottom: 8.0, - child: InkWell( - borderRadius: BorderRadius.circular(90), - onTap: widget.controller.selectAvatar, - child: const CircleAvatar( - radius: 24.0, - child: Icon( - Icons.add_a_photo_outlined, - size: 24.0, - ), - ), - ), - ), - ], - ), Flexible( child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.controller.isEditing) - ActivitySuggestionCardRow( - icon: Icons.event_note_outlined, - child: TextFormField( - controller: - widget.controller.titleController, - decoration: InputDecoration( - labelText: - L10n.of(context).activityTitle, - ), - maxLines: 2, - minLines: 1, - ), - ) - else - ActivitySuggestionCardRow( - icon: Icons.event_note_outlined, - child: Text( - widget.controller.updatedActivity.title, - style: - theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - maxLines: 6, - overflow: TextOverflow.ellipsis, - ), - ), - if (widget.controller.isEditing) - ActivitySuggestionCardRow( - icon: Symbols.target, - child: TextFormField( - controller: widget.controller - .learningObjectivesController, - decoration: InputDecoration( - labelText: L10n.of(context) - .learningObjectiveLabel, - ), - maxLines: 4, - minLines: 1, - ), - ) - else - ActivitySuggestionCardRow( - icon: Symbols.target, - child: Text( - widget.controller.updatedActivity - .learningObjective, - maxLines: 6, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge, - ), - ), - if (widget.controller.isEditing) - ActivitySuggestionCardRow( - icon: Symbols.steps, - child: TextFormField( - controller: widget - .controller.instructionsController, - decoration: InputDecoration( - labelText: - L10n.of(context).instructions, - ), - maxLines: 8, - minLines: 1, - ), - ) - else - ActivitySuggestionCardRow( - icon: Symbols.steps, - child: Text( - widget.controller.updatedActivity - .instructions, - maxLines: 8, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge, - ), - ), - if (widget.controller.isEditing) - ActivitySuggestionCardRow( - icon: Icons.group_outlined, - child: TextFormField( - controller: widget - .controller.participantsController, - decoration: InputDecoration( - labelText: L10n.of(context).classRoster, - ), - maxLines: 1, - keyboardType: TextInputType.number, - validator: (value) { - if (value == null || value.isEmpty) { - return null; - } - - try { - final val = int.parse(value); - if (val <= 0) { - return L10n.of(context) - .pleaseEnterInt; - } - } catch (e) { - return L10n.of(context) - .pleaseEnterANumber; - } - return null; - }, - ), - ) - else - ActivitySuggestionCardRow( - icon: Icons.group_outlined, - child: Text( - L10n.of(context).countParticipants( - widget.controller.updatedActivity.req - .numberOfParticipants, - ), - style: theme.textTheme.bodyLarge, - ), - ), - if (widget.controller.isEditing) - ActivitySuggestionCardRow( - icon: Symbols.dictionary, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 60.0, - ), - child: SingleChildScrollView( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: widget.controller.vocab - .mapIndexed( - (i, vocab) => Container( - padding: const EdgeInsets - .symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: theme - .colorScheme.primary - .withAlpha(20), - borderRadius: - BorderRadius.circular( - 24.0, - ), - ), - child: MouseRegion( - cursor: SystemMouseCursors - .click, - child: GestureDetector( - onTap: () => widget + child: Column( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: _width, + child: widget.controller.avatar != null + ? Image.memory( + widget.controller.avatar!, + fit: BoxFit.cover, + ) + : widget.controller.updatedActivity + .imageURL != + null + ? widget.controller.updatedActivity + .imageURL! + .startsWith("mxc") + ? MxcImage( + uri: Uri.parse( + widget .controller - .removeVocab(i), - child: Row( - spacing: 4.0, - mainAxisSize: - MainAxisSize.min, - children: [ - Text(vocab.lemma), - const Icon( - Icons.close, - size: 12.0, - ), - ], - ), + .updatedActivity + .imageURL!, ), - ), - ), - ) - .toList(), - ), - ), - ), - ) - else - ActivitySuggestionCardRow( - icon: Symbols.dictionary, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 60.0, - ), - child: SingleChildScrollView( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: widget.controller.vocab - .map( - (vocab) => Container( - padding: const EdgeInsets - .symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: theme - .colorScheme.primary - .withAlpha(20), - borderRadius: - BorderRadius.circular( - 24.0, + width: _width, + height: 200, + cacheKey: widget + .controller + .updatedActivity + .bookmarkId, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + imageUrl: widget + .controller + .updatedActivity + .imageURL!, + fit: BoxFit.cover, + placeholder: + (context, url) => + const Center( + child: + CircularProgressIndicator(), ), - ), - child: Text( - vocab.lemma, - style: theme - .textTheme.bodyMedium, - ), - ), - ) - .toList(), - ), - ), - ), + errorWidget: ( + context, + url, + error, + ) => + const SizedBox(), + ) + : null, ), - if (widget.controller.isEditing) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - ), - child: Row( - spacing: 4.0, - children: [ - Expanded( - child: TextFormField( - controller: widget - .controller.vocabController, - decoration: InputDecoration( - hintText: L10n.of(context) - .addVocabulary, - ), - maxLines: 1, - onFieldSubmitted: (_) => - widget.controller.addVocab(), + if (widget.controller.isEditing) + Positioned( + bottom: 8.0, + child: InkWell( + borderRadius: BorderRadius.circular(90), + onTap: widget.controller.selectAvatar, + child: const CircleAvatar( + radius: 24.0, + child: Icon( + Icons.add_a_photo_outlined, + size: 24.0, ), ), - IconButton( - padding: const EdgeInsets.all(0.0), - constraints: - const BoxConstraints(), // override default min size of 48px - iconSize: 16.0, - icon: const Icon(Icons.add_outlined), - onPressed: widget.controller.addVocab, - ), - ], + ), ), - ), - ], - ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Column( + children: [ + if (widget.controller.isEditing) + ActivitySuggestionCardRow( + icon: Icons.event_note_outlined, + child: TextFormField( + controller: + widget.controller.titleController, + decoration: InputDecoration( + labelText: + L10n.of(context).activityTitle, + ), + maxLines: 2, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Icons.event_note_outlined, + child: Text( + widget + .controller.updatedActivity.title, + style: theme.textTheme.titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 6, + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.controller.isEditing) + ActivitySuggestionCardRow( + icon: Symbols.target, + child: TextFormField( + controller: widget.controller + .learningObjectivesController, + decoration: InputDecoration( + labelText: L10n.of(context) + .learningObjectiveLabel, + ), + maxLines: 4, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.target, + child: Text( + widget.controller.updatedActivity + .learningObjective, + maxLines: 6, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyLarge, + ), + ), + if (widget.controller.isEditing) + ActivitySuggestionCardRow( + icon: Symbols.steps, + child: TextFormField( + controller: widget.controller + .instructionsController, + decoration: InputDecoration( + labelText: + L10n.of(context).instructions, + ), + maxLines: 8, + minLines: 1, + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.steps, + child: Text( + widget.controller.updatedActivity + .instructions, + maxLines: 8, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyLarge, + ), + ), + if (widget.controller.isEditing) + ActivitySuggestionCardRow( + icon: Icons.group_outlined, + child: TextFormField( + controller: widget.controller + .participantsController, + decoration: InputDecoration( + labelText: + L10n.of(context).classRoster, + ), + maxLines: 1, + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || + value.isEmpty) { + return null; + } + + try { + final val = int.parse(value); + if (val <= 0) { + return L10n.of(context) + .pleaseEnterInt; + } + } catch (e) { + return L10n.of(context) + .pleaseEnterANumber; + } + return null; + }, + ), + ) + else + ActivitySuggestionCardRow( + icon: Icons.group_outlined, + child: Text( + L10n.of(context).countParticipants( + widget.controller.updatedActivity + .req.numberOfParticipants, + ), + style: theme.textTheme.bodyLarge, + ), + ), + if (widget.controller.isEditing) + ActivitySuggestionCardRow( + icon: Symbols.dictionary, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 60.0, + ), + child: SingleChildScrollView( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: widget.controller.vocab + .mapIndexed( + (i, vocab) => Container( + padding: const EdgeInsets + .symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme + .colorScheme.primary + .withAlpha(20), + borderRadius: + BorderRadius + .circular( + 24.0, + ), + ), + child: MouseRegion( + cursor: + SystemMouseCursors + .click, + child: GestureDetector( + onTap: () => widget + .controller + .removeVocab(i), + child: Row( + spacing: 4.0, + mainAxisSize: + MainAxisSize + .min, + children: [ + Text(vocab.lemma), + const Icon( + Icons.close, + size: 12.0, + ), + ], + ), + ), + ), + ), + ) + .toList(), + ), + ), + ), + ) + else + ActivitySuggestionCardRow( + icon: Symbols.dictionary, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 60.0, + ), + child: SingleChildScrollView( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: widget.controller.vocab + .map( + (vocab) => Container( + padding: const EdgeInsets + .symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + color: theme + .colorScheme.primary + .withAlpha(20), + borderRadius: + BorderRadius + .circular( + 24.0, + ), + ), + child: Text( + vocab.lemma, + style: theme.textTheme + .bodyMedium, + ), + ), + ) + .toList(), + ), + ), + ), + ), + if (widget.controller.isEditing) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + ), + child: Row( + spacing: 4.0, + children: [ + Expanded( + child: TextFormField( + controller: widget + .controller.vocabController, + decoration: InputDecoration( + hintText: L10n.of(context) + .addVocabulary, + ), + maxLines: 1, + onFieldSubmitted: (_) => widget + .controller + .addVocab(), + ), + ), + IconButton( + padding: + const EdgeInsets.all(0.0), + constraints: + const BoxConstraints(), // override default min size of 48px + iconSize: 16.0, + icon: const Icon( + Icons.add_outlined, + ), + onPressed: + widget.controller.addVocab, + ), + ], + ), + ), + ], + ), + ), + ], ), ), ), @@ -459,13 +489,17 @@ class ActivitySuggestionDialogState extends State { ), ), if (widget.controller.isEditing) - GestureDetector( - child: const Icon( - Icons.close_outlined, - size: 16.0, + IconButton.filled( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primary, ), - onTap: () { - widget.controller.clearEdits(); + padding: const EdgeInsets.all(6.0), + constraints: + const BoxConstraints(), // override default min size of 48px + iconSize: 24.0, + icon: const Icon(Icons.close_outlined), + onPressed: () async { + await widget.controller.clearEdits(); widget.controller.setEditing(false); }, ) diff --git a/lib/pangea/activity_suggestions/suggestions_page.dart b/lib/pangea/activity_suggestions/suggestions_page.dart index 45beaaf97..e46be8e91 100644 --- a/lib/pangea/activity_suggestions/suggestions_page.dart +++ b/lib/pangea/activity_suggestions/suggestions_page.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; import 'package:fluffychat/pangea/public_spaces/public_spaces_area.dart'; +import 'package:fluffychat/widgets/navigation_rail.dart'; class SuggestionsPage extends StatelessWidget { const SuggestionsPage({super.key}); @@ -11,24 +15,45 @@ class SuggestionsPage extends StatelessWidget { @override Widget build(BuildContext context) { final isColumnMode = FluffyThemes.isColumnMode(context); - return SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 16.0, - ), - child: Column( - spacing: 24.0, - children: [ - if (!isColumnMode) const LearningProgressIndicators(), - const ActivitySuggestionsArea( - showTitle: true, - scrollDirection: Axis.horizontal, + return Material( + child: SafeArea( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isColumnMode && AppConfig.displayNavigationRail) ...[ + SpacesNavigationRail( + activeSpaceId: null, + onGoToChats: () => context.go('/rooms'), + onGoToSpaceId: (spaceId) => + context.go('/rooms?spaceId=$spaceId'), + ), + Container( + color: Theme.of(context).dividerColor, + width: 1, ), - const PublicSpacesArea(), ], - ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0, + ), + child: Column( + spacing: 24.0, + children: [ + if (!isColumnMode) const LearningProgressIndicators(), + const ActivitySuggestionsArea( + showTitle: true, + scrollDirection: Axis.horizontal, + ), + const PublicSpacesArea(), + ], + ), + ), + ), + ), + ], ), ), ); diff --git a/lib/pangea/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index ac28bc546..76d07588a 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -105,147 +105,156 @@ class LearningProgressIndicatorsState final mxid = client.userID ?? L10n.of(context).user; final displayname = _profile?.displayName ?? mxid.localpart ?? mxid; - return Row( - children: [ - Tooltip( - message: L10n.of(context).settings, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => context.go("/rooms/settings"), - child: Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - right: 8.0, - ), - child: Stack( - clipBehavior: Clip.none, // Allow overflow - children: [ - FutureBuilder( - future: client.fetchOwnProfile(), - builder: (context, snapshot) => Stack( - alignment: Alignment.center, - children: [ - Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(99), - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - client.userID!.localpart, - size: 60, - ), - ), - ], - ), - ), - Positioned( - bottom: -3, - right: -3, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: Theme.of(context).colorScheme.surfaceBright, - ), - padding: const EdgeInsets.all(4.0), - child: Icon( - size: 14, - Icons.settings_outlined, - color: Theme.of(context).colorScheme.primary, - weight: 1000, - ), - ), - ), - ], - ), - ), - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 6.0, - children: [ - Text( - displayname, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - LearningSettingsButton( - onTap: () => showDialog( - context: context, - builder: (c) => const SettingsLearning(), - barrierDismissible: false, - ), - l2: userL2?.langCode.toUpperCase(), - ), - ], - ), - const SizedBox(height: 6), - Row( - spacing: 6.0, - children: ConstructTypeEnum.values - .map( - (c) => ProgressIndicatorBadge( - points: uniqueLemmas(c.indicator), - loading: _loading, - onTap: () { - showDialog( - context: context, - builder: (context) => AnalyticsPopupWrapper( - view: c, - ), - ); - }, - indicator: c.indicator, - ), - ) - .toList(), - ), - const SizedBox(height: 6), - MouseRegion( + return LayoutBuilder( + builder: (context, constraints) { + return Row( + children: [ + Tooltip( + message: L10n.of(context).settings, + child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (c) => const LevelBarPopup(), - ); - }, - child: SizedBox( - height: 26, + onTap: () => context.go("/rooms/settings"), + child: Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + right: 8.0, + ), child: Stack( - alignment: Alignment.center, + clipBehavior: Clip.none, // Allow overflow children: [ - Positioned( - left: 16, - right: 0, - child: LearningProgressBar( - level: _constructsModel.level, - totalXP: _constructsModel.totalXP, + FutureBuilder( + future: client.fetchOwnProfile(), + builder: (context, snapshot) => Stack( + alignment: Alignment.center, + children: [ + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(99), + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + client.userID!.localpart, + size: 60, + ), + ), + ], ), ), Positioned( - left: 0, - child: LevelBadge(level: _constructsModel.level), + bottom: -3, + right: -3, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: + Theme.of(context).colorScheme.surfaceBright, + ), + padding: const EdgeInsets.all(4.0), + child: Icon( + size: 14, + Icons.settings_outlined, + color: Theme.of(context).colorScheme.primary, + weight: 1000, + ), + ), ), ], ), ), ), ), - ], - ), - ), - ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 6.0, + children: [ + Text( + displayname, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + LearningSettingsButton( + onTap: () => showDialog( + context: context, + builder: (c) => const SettingsLearning(), + barrierDismissible: false, + ), + l2: userL2?.langCode.toUpperCase(), + ), + ], + ), + const SizedBox(height: 6), + Row( + spacing: 6.0, + children: ConstructTypeEnum.values + .map( + (c) => ProgressIndicatorBadge( + points: uniqueLemmas(c.indicator), + loading: _loading, + onTap: () { + showDialog( + context: context, + builder: (context) => AnalyticsPopupWrapper( + view: c, + ), + ); + }, + indicator: c.indicator, + mini: constraints.maxWidth < 300, + ), + ) + .toList(), + ), + const SizedBox(height: 6), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (c) => const LevelBarPopup(), + ); + }, + child: SizedBox( + height: 26, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + left: 16, + right: 0, + child: LearningProgressBar( + level: _constructsModel.level, + totalXP: _constructsModel.totalXP, + ), + ), + Positioned( + left: 0, + child: LevelBadge( + level: _constructsModel.level, + mini: constraints.maxWidth < 300, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ); + }, ); } } diff --git a/lib/pangea/analytics_summary/level_badge.dart b/lib/pangea/analytics_summary/level_badge.dart index 1282c1c41..f1bd6da1b 100644 --- a/lib/pangea/analytics_summary/level_badge.dart +++ b/lib/pangea/analytics_summary/level_badge.dart @@ -2,14 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/analytics_summary/level_bar_popup.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; class LevelBadge extends StatelessWidget { final int level; + final bool mini; + const LevelBadge({ required this.level, + this.mini = false, super.key, }); @@ -31,29 +33,13 @@ class LevelBadge extends StatelessWidget { color: Theme.of(context).colorScheme.surfaceBright, ), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - backgroundColor: AppConfig.gold, - radius: 8, - child: Icon( - size: 12, - Icons.star, - color: Theme.of(context).colorScheme.surfaceBright, - weight: 1000, - ), - ), - const SizedBox(width: 4), - Text( - L10n.of(context).levelShort(level), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], + child: Text( + "⭐ ${mini ? "$level" : L10n.of(context).levelShort(level)}", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), ), ), ); diff --git a/lib/pangea/analytics_summary/level_bar_popup.dart b/lib/pangea/analytics_summary/level_bar_popup.dart index 700b9fee8..8d0ec2e30 100644 --- a/lib/pangea/analytics_summary/level_bar_popup.dart +++ b/lib/pangea/analytics_summary/level_bar_popup.dart @@ -43,27 +43,13 @@ class LevelBarPopup extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - const CircleAvatar( - radius: 20, - backgroundColor: AppConfig.gold, - child: Icon( - size: 30, - Icons.star, - color: Colors.white, - ), - ), - const SizedBox(width: 10), - Text( - L10n.of(context).levelShort(level), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w900, - color: AppConfig.gold, - ), - ), - ], + Text( + "⭐ ${L10n.of(context).levelShort(level)}", + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: AppConfig.gold, + ), ), Opacity( opacity: 0.25, diff --git a/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart b/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart index 72144756d..7257a4f4b 100644 --- a/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart +++ b/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart @@ -95,14 +95,6 @@ class AnimatedLevelBarState extends State borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(50), - spreadRadius: 0, - blurRadius: 5, - offset: const Offset(5, 0), - ), - ], ), ), Positioned( diff --git a/lib/pangea/analytics_summary/progress_indicator.dart b/lib/pangea/analytics_summary/progress_indicator.dart index faaaa46b6..aa99cf40d 100644 --- a/lib/pangea/analytics_summary/progress_indicator.dart +++ b/lib/pangea/analytics_summary/progress_indicator.dart @@ -9,6 +9,7 @@ class ProgressIndicatorBadge extends StatelessWidget { final int points; final VoidCallback onTap; final ProgressIndicatorEnum indicator; + final bool mini; const ProgressIndicatorBadge({ super.key, @@ -16,6 +17,7 @@ class ProgressIndicatorBadge extends StatelessWidget { required this.indicator, required this.loading, required this.points, + this.mini = false, }); @override @@ -42,15 +44,17 @@ class ProgressIndicatorBadge extends StatelessWidget { color: indicator.color(context), weight: 1000, ), - const SizedBox(width: 4.0), - Text( - indicator.tooltip(context), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: indicator.color(context), + if (!mini) ...[ + const SizedBox(width: 4.0), + Text( + indicator.tooltip(context), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: indicator.color(context), + ), ), - ), + ], const SizedBox(width: 4.0), !loading ? Text( diff --git a/lib/pangea/chat/constants/default_power_level.dart b/lib/pangea/chat/constants/default_power_level.dart index 4bb21ef1b..1459199e6 100644 --- a/lib/pangea/chat/constants/default_power_level.dart +++ b/lib/pangea/chat/constants/default_power_level.dart @@ -1,32 +1,60 @@ Map defaultPowerLevels(String userID) => { + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, "events": { - "m.room.avatar": 50, - "m.room.canonical_alias": 50, - "m.room.encryption": 100, - "m.room.history_visibility": 100, - "m.room.name": 50, "m.room.power_levels": 100, - "m.room.server_acl": 100, - "m.room.tombstone": 100, + "m.room.pinned_events": 50, }, + "events_default": 0, + "state_default": 50, "users": { userID: 100, }, + "users_default": 0, + "notifications": { + "room": 50, + }, }; Map restrictedPowerLevels(String userID) => { - "events_default": 50, + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, "events": { - "m.room.avatar": 50, - "m.room.canonical_alias": 50, - "m.room.encryption": 100, - "m.room.history_visibility": 100, - "m.room.name": 50, "m.room.power_levels": 100, - "m.room.server_acl": 100, - "m.room.tombstone": 100, + "m.room.pinned_events": 50, }, + "events_default": 50, + "state_default": 50, "users": { userID: 100, }, + "users_default": 0, + "notifications": { + "room": 50, + }, + }; + +Map defaultSpacePowerLevels(String userID) => { + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, + "events": { + "m.room.power_levels": 100, + "m.room.join_rules": 100, + "m.space.child": 50, + }, + "events_default": 0, + "state_default": 50, + "users": { + userID: 100, + }, + "users_default": 0, + "notifications": { + "room": 50, + }, }; diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 9be056e3a..ef5364270 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -453,11 +453,6 @@ class Choreographer { if (!isNormalizationError) continue; final match = igc.igcTextData!.matches[i]; - choreoRecord.addRecord( - _textController.text, - match: match.copyWith..status = PangeaMatchStatus.automatic, - ); - igc.igcTextData!.acceptReplacement( i, match.match.choices!.indexWhere( @@ -465,6 +460,19 @@ class Choreographer { ), ); + final newMatch = match.copyWith; + newMatch.status = PangeaMatchStatus.automatic; + newMatch.match.length = match.match.choices! + .firstWhere((c) => c.isBestCorrection) + .value + .characters + .length; + + choreoRecord.addRecord( + _textController.text, + match: newMatch, + ); + _textController.setSystemText( igc.igcTextData!.originalInput, EditType.igc, diff --git a/lib/pangea/choreographer/controllers/error_service.dart b/lib/pangea/choreographer/controllers/error_service.dart index 94d1c83f3..2021815ff 100644 --- a/lib/pangea/choreographer/controllers/error_service.dart +++ b/lib/pangea/choreographer/controllers/error_service.dart @@ -65,7 +65,17 @@ class ErrorService { return Duration(seconds: coolDownSeconds); } + final List _errorCache = []; + setError(ChoreoError? error, {Duration? duration}) { + if (_errorCache.contains(error?.raw.toString())) { + return; + } + + if (error != null) { + _errorCache.add(error.raw.toString()); + } + _error = error; Future.delayed(duration ?? defaultCooldown, () { clear(); diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 76eaed5cb..8af6dc802 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -295,6 +295,9 @@ class IgcController { igcTextData = null; spanDataController.clearCache(); spanDataController.dispose(); + MatrixState.pAnyState.closeAllOverlays( + filter: RegExp(r'span_card_overlay_\d+'), + ); } dispose() { diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index ac6613b58..3991aa4f7 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -292,12 +292,45 @@ class IGCTextData { // create a pointer to the current index in the original input // and iterate until the pointer has reached the end of the input int currentIndex = 0; + int loops = 0; + final List addedMatches = []; while (currentIndex < originalInput.characters.length) { + if (loops > 100) { + ErrorHandler.logError( + e: "In constructTokenSpan, infinite loop detected", + data: { + "currentIndex": currentIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, infinite loop detected"; + } + // check if the pointer is at a match, and if so, get the index of the match final int matchIndex = matchRanges.indexWhere( (range) => currentIndex >= range[0] && currentIndex < range[1], ); - final bool inMatch = matchIndex != -1; + final bool inMatch = matchIndex != -1 && + !addedMatches.contains( + textSpanMatches[matchIndex], + ); + + if (matchIndex != -1 && + addedMatches.contains( + textSpanMatches[matchIndex], + )) { + ErrorHandler.logError( + e: "In constructTokenSpan, currentIndex is in match that has already been added", + data: { + "currentIndex": currentIndex, + "matchIndex": matchIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, currentIndex is in match that has already been added"; + } + + final prevIndex = currentIndex; if (inMatch) { // if the pointer is in a match, then add that match to items @@ -312,13 +345,7 @@ class IGCTextData { final span = originalInput.characters .getRange( match.match.offset, - match.match.offset + - (match.match.choices - ?.firstWhere((c) => c.isBestCorrection) - .value - .characters - .length ?? - match.match.length), + match.match.offset + match.match.length, ) .toString(); @@ -364,12 +391,8 @@ class IGCTextData { ), ); - currentIndex = match.match.offset + - (match.match.choices - ?.firstWhere((c) => c.isBestCorrection) - .value - .length ?? - match.match.length); + addedMatches.add(match); + currentIndex = match.match.offset + match.match.length; } else { items.add( getSpanItem( @@ -400,6 +423,20 @@ class IGCTextData { ); currentIndex = nextIndex; } + + if (prevIndex >= currentIndex) { + ErrorHandler.logError( + e: "In constructTokenSpan, currentIndex is less than prevIndex", + data: { + "currentIndex": currentIndex, + "prevIndex": prevIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, currentIndex is less than prevIndex"; + } + + loops++; } return items; diff --git a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart index d887eebbf..50ad90e68 100644 --- a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart'; @@ -174,18 +175,29 @@ class PangeaTextController extends TextEditingController { final choreoSteps = choreographer.choreoRecord.choreoSteps; + List inlineSpans = []; + try { + inlineSpans = choreographer.igc.igcTextData!.constructTokenSpan( + choreoSteps: choreoSteps.isNotEmpty && + choreoSteps.last.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.automatic + ? choreoSteps + : [], + defaultStyle: style, + onUndo: choreographer.onUndoReplacement, + ); + } catch (e) { + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: e), + ); + inlineSpans = [TextSpan(text: text, style: style)]; + choreographer.igc.clear(); + } + return TextSpan( style: style, children: [ - ...choreographer.igc.igcTextData!.constructTokenSpan( - choreoSteps: choreoSteps.isNotEmpty && - choreoSteps.last.acceptedOrIgnoredMatch?.status == - PangeaMatchStatus.automatic - ? choreoSteps - : [], - defaultStyle: style, - onUndo: choreographer.onUndoReplacement, - ), + ...inlineSpans, TextSpan(text: parts[1], style: style), ], ); diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index 7d67a074f..5018d6b17 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -11,6 +11,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/choreographer/controllers/contextual_definition_controller.dart'; @@ -268,6 +269,13 @@ class PangeaController { preset: CreateRoomPreset.trustedPrivateChat, initialState: [ BotOptionsModel(mode: BotMode.directChat).toStateEvent, + StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: defaultPowerLevels( + matrixState.client.userID!, + ), + ), ], ); diff --git a/lib/pangea/events/event_wrappers/pangea_representation_event.dart b/lib/pangea/events/event_wrappers/pangea_representation_event.dart index 7e10fd14b..b7607e36e 100644 --- a/lib/pangea/events/event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_representation_event.dart @@ -93,7 +93,7 @@ class RepresentationEvent { if (tokenEvents.isEmpty) return null; if (tokenEvents.length > 1) { - debugger(when: kDebugMode); + // debugger(when: kDebugMode); Sentry.addBreadcrumb( Breadcrumb( message: diff --git a/lib/pangea/layouts/bottom_nav_layout.dart b/lib/pangea/layouts/bottom_nav_layout.dart deleted file mode 100644 index 3332a673d..000000000 --- a/lib/pangea/layouts/bottom_nav_layout.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - -class BottomNavLayout extends StatelessWidget { - final Widget mainView; - - const BottomNavLayout({ - super.key, - required this.mainView, - }); - @override - Widget build(BuildContext context) { - return Scaffold( - body: mainView, - bottomNavigationBar: const BottomNavBar(), - ); - } -} - -class BottomNavBar extends StatefulWidget { - const BottomNavBar({ - super.key, - }); - - @override - BottomNavBarState createState() => BottomNavBarState(); -} - -class BottomNavBarState extends State { - int get selectedIndex { - final route = GoRouterState.of(context).fullPath.toString(); - if (route.contains("settings")) { - return 2; - } - if (route.contains('homepage')) { - return 0; - } - return 1; - } - - void onItemTapped(int index) { - switch (index) { - case 0: - context.go('/rooms/homepage'); - break; - case 1: - context.go('/rooms'); - break; - case 2: - context.go('/rooms/settings'); - break; - } - - if (mounted) setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.primary.withAlpha(50), - ), - ), - ), - child: BottomNavigationBar( - iconSize: 16.0, - onTap: onItemTapped, - selectedItemColor: Theme.of(context).colorScheme.primary, - selectedFontSize: 14.0, - unselectedFontSize: 14.0, - currentIndex: selectedIndex, - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.home_outlined), - activeIcon: const Icon(Icons.home), - label: L10n.of(context).home, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.chat_bubble_outline), - activeIcon: const Icon(Icons.chat_bubble), - label: L10n.of(context).chats, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.settings_outlined), - activeIcon: const Icon(Icons.settings), - label: L10n.of(context).settings, - ), - ], - ), - ); - } -} diff --git a/lib/pangea/spaces/utils/client_spaces_extension.dart b/lib/pangea/spaces/utils/client_spaces_extension.dart index 8f6f6e759..fe1345768 100644 --- a/lib/pangea/spaces/utils/client_spaces_extension.dart +++ b/lib/pangea/spaces/utils/client_spaces_extension.dart @@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/spaces/utils/space_code.dart'; extension SpacesClientExtension on Client { Future createPangeaSpace({ required String name, + required String introChatName, + required String announcementsChatName, Visibility visibility = Visibility.private, JoinRules joinRules = JoinRules.public, Uint8List? avatar, @@ -24,6 +26,7 @@ extension SpacesClientExtension on Client { powerLevelContentOverride: {'events_default': 100}, initialState: [ ..._spaceInitialState( + userID!, joinCode, joinRules: joinRules, ), @@ -38,7 +41,11 @@ extension SpacesClientExtension on Client { final space = await _waitForRoom(roomId); if (space == null) return roomId; - await _addDefaultSpaceChats(space: space); + await _addDefaultSpaceChats( + space: space, + introductionsName: introChatName, + announcementsName: announcementsChatName, + ); return roomId; } @@ -108,6 +115,13 @@ extension SpacesClientExtension on Client { throw Exception('Failed to create default space chats'); } + for (final roomId in roomIds) { + final room = getRoomById(roomId); + if (room == null) { + await waitForRoomInSync(roomId, join: true); + } + } + final addIntroChatFuture = space.pangeaSetSpaceChild( roomIds[0], ); @@ -123,6 +137,7 @@ extension SpacesClientExtension on Client { } List _spaceInitialState( + String userID, String joinCode, { required JoinRules joinRules, }) { @@ -130,15 +145,7 @@ extension SpacesClientExtension on Client { StateEvent( type: EventTypes.RoomPowerLevels, stateKey: '', - content: { - 'events': { - EventTypes.SpaceChild: 50, - }, - 'users_default': 0, - 'users': { - userID: SpaceConstants.powerLevelOfAdmin, - }, - }, + content: defaultSpacePowerLevels(userID), ), StateEvent( type: EventTypes.RoomJoinRules, diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 7c071fbc6..1dea15a8d 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -484,6 +484,12 @@ class MatrixState extends State with WidgetsBindingObserver { AppConfig.showPresences = store.getBool(SettingKeys.showPresences) ?? AppConfig.showPresences; + + // #Pangea + AppConfig.displayNavigationRail = + store.getBool(SettingKeys.displayNavigationRail) ?? + AppConfig.displayNavigationRail; + // Pangea# } @override diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index d3def0630..7ae9665b5 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -34,6 +34,15 @@ class SpacesNavigationRail extends StatelessWidget { .uri .path .startsWith('/rooms/settings'); + // #Pangea + final isHomepage = GoRouter.of(context) + .routeInformationProvider + .value + .uri + .path + .contains('homepage'); + final isColumnMode = FluffyThemes.isColumnMode(context); + // Pangea# return StreamBuilder( key: ValueKey( client.userID.toString(), @@ -53,7 +62,12 @@ class SpacesNavigationRail extends StatelessWidget { .toList(); return SizedBox( - width: FluffyThemes.navRailWidth, + // #Pangea + // width: FluffyThemes.navRailWidth, + width: isColumnMode + ? FluffyThemes.navRailWidth + : FluffyThemes.navRailWidth * 0.75, + // Pangea# child: Column( children: [ Expanded( @@ -61,35 +75,56 @@ class SpacesNavigationRail extends StatelessWidget { scrollDirection: Axis.vertical, // #Pangea // itemCount: rootSpaces.length + 2, - itemCount: rootSpaces.length + 3, + itemCount: rootSpaces.length + 4, // Pangea# itemBuilder: (context, i) { + // #Pangea if (i == 0) { return NaviRailItem( - isSelected: activeSpaceId == null && !isSettings, - onTap: onGoToChats, + isSelected: isColumnMode + ? activeSpaceId == null && !isSettings + : isHomepage, + onTap: () => isColumnMode + ? onGoToChats() + : context.go("/rooms/homepage"), icon: const Padding( padding: EdgeInsets.all(10.0), - // #Pangea - // child: Icon(Icons.forum_outlined), child: Icon(Icons.home_outlined), - // Pangea# ), selectedIcon: const Padding( padding: EdgeInsets.all(10.0), - // #Pangea - // child: Icon(Icons.forum), child: Icon(Icons.home), - // Pangea# ), - // #Pangea - // toolTip: L10n.of(context).chats, toolTip: L10n.of(context).home, - // Pangea# unreadBadgeFilter: (room) => true, ); } i--; + // Pangea# + if (i == 0) { + return isColumnMode + ? const SizedBox() + : NaviRailItem( + // #Pangea + // isSelected: activeSpaceId == null && !isSettings, + isSelected: activeSpaceId == null && + !isSettings && + !isHomepage, + // Pangea# + onTap: onGoToChats, + icon: const Padding( + padding: EdgeInsets.all(10.0), + child: Icon(Icons.forum_outlined), + ), + selectedIcon: const Padding( + padding: EdgeInsets.all(10.0), + child: Icon(Icons.forum), + ), + toolTip: L10n.of(context).chats, + unreadBadgeFilter: (room) => true, + ); + } + i--; if (i == rootSpaces.length) { // #Pangea return NaviRailItem(