Merge branch 'main' of https://github.com/pangeachat/client into remove-duplicate-push

This commit is contained in:
Kelrap 2025-05-22 12:57:44 -04:00
commit 031f985d11
34 changed files with 1078 additions and 885 deletions

View file

@ -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"
}

View file

@ -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;

View file

@ -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#
}

View file

@ -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<T> {

View file

@ -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,
]

View file

@ -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,

View file

@ -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(

View file

@ -527,6 +527,18 @@ class _SpaceViewState extends State<SpaceView> {
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<Room>()
.toList();
// Pangea#
return Scaffold(
// #Pangea
// appBar: AppBar(
@ -539,14 +551,51 @@ class _SpaceViewState extends State<SpaceView> {
_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<SpaceView> {
// Pangea#
.toList();
final joinedParents = room.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
// #Pangea
// final joinedParents = room.spaceParents
// .map((parent) {
// final roomId = parent.roomId;
// if (roomId == null) return null;
// return room.client.getRoomById(roomId);
// })
// .whereType<Room>()
// .toList();
// Pangea#
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
@ -715,51 +766,51 @@ class _SpaceViewState extends State<SpaceView> {
),
),
),
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(

View file

@ -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<String, Object?>.from(
room.getState(EventTypes.RoomPowerLevels)?.content ?? {},
);
final powerLevels =
Map<String, dynamic>.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<String, dynamic>.from(powerLevelsContent)
..removeWhere((k, v) => v is! int);
final eventsPowerLevels = Map<String, int?>.from(
powerLevelsContent.tryGetMap<String, int?>('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<String, Object?>('notifications')
// ?.tryGet<int>('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<String, Object?>('notifications')
?.tryGet<int>('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#
),
],
),

View file

@ -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#
}
}

View file

@ -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<NewGroup> {
);
}
}
// if a timeout happened, don't redirect to the chat
if (error != null) return;
// Pangea#
context.go('/rooms/$roomId/invite?filter=groups');
// Pangea#
}
Future<void> _createSpace() async {
@ -220,6 +217,8 @@ class NewGroupController extends State<NewGroup> {
// context.pop<String>(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<NewGroup> {
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<NewGroup> {
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);

View file

@ -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'),

View file

@ -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#
],
),
),

View file

@ -211,7 +211,7 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
}
Future<void> clearEdits() async {
_resetActivity();
await _resetActivity();
if (mounted) {
setState(() {
isEditing = false;

View file

@ -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<ActivityRoomSelection> {
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,

View file

@ -86,320 +86,350 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
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<ActivitySuggestionDialog> {
),
),
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);
},
)

View file

@ -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(),
],
),
),
),
),
],
),
),
);

View file

@ -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<Profile>(
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<AnalyticsPopupWrapper>(
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<LevelBarPopup>(
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<Profile>(
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<AnalyticsPopupWrapper>(
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<LevelBarPopup>(
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,
),
),
],
),
),
),
),
],
),
),
],
);
},
);
}
}

View file

@ -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,
),
),
),
);

View file

@ -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,

View file

@ -95,14 +95,6 @@ class AnimatedLevelBarState extends State<AnimatedLevelBar>
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(

View file

@ -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(

View file

@ -1,32 +1,60 @@
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
},
};

View file

@ -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,

View file

@ -65,7 +65,17 @@ class ErrorService {
return Duration(seconds: coolDownSeconds);
}
final List<String> _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();

View file

@ -295,6 +295,9 @@ class IgcController {
igcTextData = null;
spanDataController.clearCache();
spanDataController.dispose();
MatrixState.pAnyState.closeAllOverlays(
filter: RegExp(r'span_card_overlay_\d+'),
);
}
dispose() {

View file

@ -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<PangeaMatch> 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;

View file

@ -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<InlineSpan> 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),
],
);

View file

@ -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!,
),
),
],
);

View file

@ -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:

View file

@ -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<BottomNavBar> {
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,
),
],
),
);
}
}

View file

@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
extension SpacesClientExtension on Client {
Future<String> 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<StateEvent> _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,

View file

@ -484,6 +484,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
AppConfig.showPresences =
store.getBool(SettingKeys.showPresences) ?? AppConfig.showPresences;
// #Pangea
AppConfig.displayNavigationRail =
store.getBool(SettingKeys.displayNavigationRail) ??
AppConfig.displayNavigationRail;
// Pangea#
}
@override

View file

@ -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(