feat: redesign of space access page (#2903)

This commit is contained in:
ggurdin 2025-06-02 14:48:39 -04:00 committed by GitHub
parent f7a8ef9afd
commit 249538c20b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 617 additions and 618 deletions

View file

@ -4962,5 +4962,18 @@
"access": "Access",
"addSubspace": "Add subspace",
"botSettings": "Bot settings",
"activitySuggestionTimeoutMessage": "We are working hard to generate activties for you, please check back in a minute"
"activitySuggestionTimeoutMessage": "We are working hard to generate activties for you, please check back in a minute",
"accessSettingsWarning": "Oops! It looks like you don't have permission to set the Access rules of this room. You should check these to make sure they're what you need and talk to a room admin if you need to change them",
"howSpaceCanBeFound": "How this space can be found",
"private": "Private",
"cannotBeFoundInSearch": "Cannot be found in search",
"public": "Public",
"visibleToCommunity": "Visible to the broader Pangea Chat community via \"Find your people\"",
"howSpaceCanBeJoined": "How this space can be joined",
"restricted": "Restricted",
"canBeFoundVia": "Can be found via:",
"canBeFoundViaInvitation": "\u2022 invitation",
"canBeFoundViaCodeOrLink": "\u2022 code or link",
"canBeFoundViaKnock": "\u2022 request to join and admin approval",
"anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!"
}

View file

@ -202,7 +202,6 @@ abstract class AppRoutes {
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
@ -245,7 +244,6 @@ abstract class AppRoutes {
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
),
),
@ -287,22 +285,12 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const NewGroup(),
// #Pangea
// const NewGroup(),
NewGroup(spaceId: state.uri.queryParameters['space']),
// Pangea#
),
redirect: loggedOutRedirect,
// #Pangea
routes: [
GoRoute(
path: ':spaceid',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
NewGroup(spaceId: state.pathParameters['spaceid']!),
),
redirect: loggedOutRedirect,
),
],
// Pangea#
),
GoRoute(
path: 'newspace',
@ -775,7 +763,6 @@ abstract class AppRoutes {
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),

View file

@ -901,7 +901,7 @@ class ChatController extends State<ChatPageWithRoom>
pangeaEditingEvent = previousEdit;
}
final spaceCode = room.classCode(context);
final spaceCode = room.classCode;
if (spaceCode != null) {
GoogleAnalytics.sendMessage(
room.id,

View file

@ -4,7 +4,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_page.dart';
import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_access_settings.dart';
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -58,13 +59,22 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
}
void setJoinRule(JoinRules? newJoinRules) async {
if (newJoinRules == null) return;
// #Pangea
// if (newJoinRules == null) return;
if (newJoinRules == null || room.joinRules == newJoinRules) return;
// Pangea#
setState(() {
joinRulesLoading = true;
});
try {
await room.setJoinRules(newJoinRules);
// #Pangea
// await room.setJoinRules(newJoinRules);
await room.client.pangeaSetJoinRules(
room.id,
newJoinRules.toString().replaceAll('JoinRules.', ''),
);
// Pangea#
} catch (e, s) {
Logs().w('Unable to change join rules', e, s);
if (mounted) {
@ -295,6 +305,9 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
@override
Widget build(BuildContext context) {
return ChatAccessSettingsPageView(this);
// #Pangea
// return ChatAccessSettingsPageView(this);
return PangeaChatAccessSettingsPageView(this);
// Pangea#
}
}

View file

@ -6,18 +6,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart';
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/utils/set_class_name.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
@ -209,70 +211,6 @@ class ChatDetailsController extends State<ChatDetails> {
// Pangea#
// #Pangea
bool showEditNameIcon = false;
void hoverEditNameIcon(bool hovering) =>
setState(() => showEditNameIcon = !showEditNameIcon);
Future<void> setJoinRules(JoinRules joinRules) async {
if (roomId == null) return;
final room = Matrix.of(context).client.getRoomById(roomId!);
if (room == null) return;
final content = room.getState(EventTypes.RoomJoinRules)?.content ?? {};
content['join_rule'] = joinRules.toString().replaceAll('JoinRules.', '');
await showFutureLoadingDialog(
context: context,
future: () async {
await room.client.setRoomStateWithKey(
roomId!,
EventTypes.RoomJoinRules,
'',
content,
);
},
);
}
Future<void> setVisibility(sdk.Visibility visibility) async {
if (roomId == null) return;
final room = Matrix.of(context).client.getRoomById(roomId!);
if (room == null) return;
await showFutureLoadingDialog(
context: context,
future: () async {
await room.client.setRoomVisibilityOnDirectory(
room.id,
visibility: visibility,
);
},
);
setState(() {});
}
Future<void> toggleMute() async {
final client = Matrix.of(context).client;
final Room? room = client.getRoomById(roomId!);
if (room == null) return;
await showFutureLoadingDialog(
context: context,
future: () async {
await (room.pushRuleState == PushRuleState.notify
? room.setPushRuleState(PushRuleState.mentionsOnly)
: room.setPushRuleState(PushRuleState.notify));
},
);
// wait for push rule update in sync
await client.onSync.stream.firstWhere(
(sync) =>
sync.accountData != null &&
sync.accountData!.isNotEmpty &&
sync.accountData!.any((e) => e.type == 'm.push_rules'),
);
if (mounted) setState(() {});
}
void downloadChatAction() async {
if (roomId == null) return;
final Room? room = Matrix.of(context).client.getRoomById(roomId!);
@ -389,22 +327,37 @@ class ChatDetailsController extends State<ChatDetails> {
);
if (names == null) return;
final client = Matrix.of(context).client;
final result = await showFutureLoadingDialog(
await showFutureLoadingDialog(
context: context,
future: () async {
final activeSpace = client.getRoomById(roomId!)!;
await activeSpace.postLoad();
final accessCode = await SpaceCodeUtil.generateSpaceCode(client);
final resp = await client.createSpace(
final resp = await client.createRoom(
name: names,
visibility: activeSpace.joinRules == JoinRules.public
? sdk.Visibility.public
: sdk.Visibility.private,
visibility: RoomDefaults.spaceChildVisibility,
creationContent: {'type': 'm.space'},
initialState: [
RoomDefaults.defaultSpacePowerLevels(client.userID!),
StateEvent(
type: EventTypes.RoomJoinRules,
content: {
'join_rule': 'knock_restricted',
'allow': [
{
"type": "m.room_membership",
"room_id": roomId!,
}
],
ModelKey.accessCode: accessCode,
},
),
],
);
await activeSpace.pangeaSetSpaceChild(resp);
await activeSpace.addToSpace(resp);
},
);
if (result.error != null) return;
}
// Pangea#
}

View file

@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@ -83,7 +84,6 @@ class ChatList extends StatefulWidget {
final String? activeChat;
// #Pangea
final String? activeSpaceId;
final String? activeFilter;
// Pangea#
final bool displayNavigationRail;
@ -92,7 +92,6 @@ class ChatList extends StatefulWidget {
required this.activeChat,
// #Pangea
this.activeSpaceId,
this.activeFilter,
// Pangea#
this.displayNavigationRail = false,
});
@ -535,7 +534,7 @@ class ChatListController extends State<ChatList>
// #Pangea
final String? justInputtedCode =
MatrixState.pangeaController.classController.justInputtedCode();
final newSpaceCode = space?.classCode(context);
final newSpaceCode = space?.classCode;
if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) {
return;
}
@ -625,12 +624,6 @@ class ChatListController extends State<ChatList>
_activeSpaceId =
widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId;
if (widget.activeFilter == 'groups') {
activeFilter = AppConfig.separateChatTypes
? ActiveFilter.groups
: ActiveFilter.allChats;
}
// Pangea#
super.initState();
@ -640,15 +633,6 @@ class ChatListController extends State<ChatList>
@override
void didUpdateWidget(ChatList oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.activeFilter != oldWidget.activeFilter &&
widget.activeFilter == 'groups') {
setActiveFilter(
AppConfig.separateChatTypes
? ActiveFilter.groups
: ActiveFilter.allChats,
);
}
if (widget.activeSpaceId != oldWidget.activeSpaceId &&
widget.activeSpaceId != null) {
widget.activeSpaceId == 'clear'
@ -822,12 +806,11 @@ class ChatListController extends State<ChatList>
],
),
),
if (spacesWithPowerLevels.isNotEmpty
// #Pangea
&&
!room.isSpace
// Pangea#
)
// #Pangea
// if (spacesWithPowerLevels.isNotEmpty)
if (spacesWithPowerLevels.isNotEmpty &&
room.canChangeStateEvent(EventTypes.SpaceParent))
// Pangea#
PopupMenuItem(
value: ChatContextAction.addToSpace,
child: Row(
@ -846,8 +829,11 @@ class ChatListController extends State<ChatList>
// if the room has a parent for which the user has a high enough power level
// to set parent's space child events, show option to remove the room from the space
if (room.spaceParents.isNotEmpty &&
room.canChangeStateEvent(EventTypes.SpaceParent) &&
room.pangeaSpaceParents.any(
(r) => r.canChangeStateEvent(EventTypes.SpaceChild),
(r) =>
r.canChangeStateEvent(EventTypes.SpaceChild) &&
r.id == activeSpaceId,
) &&
activeSpaceId != null)
PopupMenuItem(
@ -1022,21 +1008,40 @@ class ChatListController extends State<ChatList>
context: context,
// #Pangea
// future: () => space.setSpaceChild(room.id),
future: () => space.pangeaSetSpaceChild(room.id),
future: () => space.addToSpace(room.id),
// Pangea#
);
// #Pangea
try {
await space.client.setSpaceChildAccess(room.id);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).accessSettingsWarning),
duration: const Duration(seconds: 10),
),
);
}
return;
case ChatContextAction.removeFromSpace:
await showFutureLoadingDialog(
context: context,
future: () async {
final futures = room.pangeaSpaceParents
.where((r) => r.canChangeStateEvent(EventTypes.SpaceChild))
.map((space) => removeSpaceChild(space, room.id));
await Future.wait(futures);
final activeSpace = room.client.getRoomById(activeSpaceId!);
if (activeSpace == null) return;
await activeSpace.removeSpaceChild(room.id);
},
);
try {
await room.client.resetSpaceChildAccess(room.id);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).accessSettingsWarning),
duration: const Duration(seconds: 10),
),
);
}
return;
case ChatContextAction.delete:
if (room.isSpace) {
@ -1077,22 +1082,6 @@ class ChatListController extends State<ChatList>
}
}
// #Pangea
/// Remove a room from a space. Often, the user will have permission to set
/// the SpaceChild event for the parent space, but not the SpaceParent event.
/// This would cause a permissions error, but the child will still be removed
/// via the SpaceChild event. If that's the case, silence the error.
Future<void> removeSpaceChild(Room space, String roomId) async {
try {
await space.removeSpaceChild(roomId);
} catch (err) {
if ((err as MatrixException).error != MatrixError.M_FORBIDDEN) {
rethrow;
}
}
}
// Pangea#
void dismissStatusList() async {
final result = await showOkCancelAlertDialog(
title: L10n.of(context).hidePresences,

View file

@ -7,7 +7,6 @@ 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';
import 'package:fluffychat/pangea/spaces/widgets/space_floating_actions_buttons.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
class ChatListView extends StatelessWidget {
@ -58,30 +57,17 @@ class ChatListView extends StatelessWidget {
// body: ChatListViewBody(controller),
body: ChatListViewBodyWrapper(controller: controller),
// Pangea#
floatingActionButton:
// #Pangea
// !controller.isSearchMode && controller.activeSpaceId == null
controller.activeFilter == ActiveFilter.spaces &&
controller.activeSpaceId == null &&
!controller.isSearchMode
? const SpaceFloatingActionButtons()
: !controller.isSearchMode &&
controller.activeSpaceId == null
// Pangea#
? FloatingActionButton.extended(
// #Pangea
// onPressed: () => context.go('/rooms/newprivatechat'),
onPressed: () => context.go(
'/rooms/newgroup/${controller.activeSpaceId ?? ''}',
),
// Pangea#
icon: const Icon(Icons.add_outlined),
label: Text(
L10n.of(context).chat,
overflow: TextOverflow.fade,
),
)
: const SizedBox.shrink(),
floatingActionButton: !controller.isSearchMode &&
controller.activeSpaceId == null
? FloatingActionButton.extended(
onPressed: () => context.go('/rooms/newprivatechat'),
icon: const Icon(Icons.add_outlined),
label: Text(
L10n.of(context).chat,
overflow: TextOverflow.fade,
),
)
: const SizedBox.shrink(),
),
),
),

View file

@ -678,7 +678,8 @@ class _SpaceViewState extends State<SpaceView> {
// #Pangea
// onPressed: _addChatOrSubspace,
// label: Text(L10n.of(context).group),
onPressed: () => context.go("/rooms/newgroup/${widget.spaceId}"),
onPressed: () =>
context.go("/rooms/newgroup?space=${widget.spaceId}"),
label: Text(L10n.of(context).chat),
// Pangea#
icon: const Icon(Icons.group_add_outlined),

View file

@ -54,17 +54,17 @@ class InvitationSelectionView extends StatelessWidget {
title: Text(L10n.of(context).inviteContact),
// #Pangea
actions: [
if (room.isSpace && room.classCode(context) != null)
if (room.isSpace && room.classCode != null)
PopupMenuButton<int>(
icon: const Icon(Icons.share_outlined),
onSelected: (value) async {
final spaceCode = room.classCode(context)!;
final spaceCode = room.classCode!;
String toCopy = spaceCode;
if (value == 0) {
final String initialUrl =
kIsWeb ? html.window.origin! : Environment.frontendURL;
toCopy =
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}";
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode}";
}
await Clipboard.setData(ClipboardData(text: toCopy));
@ -92,8 +92,7 @@ class InvitationSelectionView extends StatelessWidget {
child: ListTile(
leading: const Icon(Icons.share_outlined),
title: Text(
L10n.of(context)
.shareInviteCode(room.classCode(context)!),
L10n.of(context).shareInviteCode(room.classCode!),
),
contentPadding: const EdgeInsets.all(0),
),

View file

@ -140,11 +140,22 @@ class NewGroupController extends State<NewGroup> {
content: {'url': avatarUrl.toString()},
),
// #Pangea
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(Matrix.of(context).client.userID!),
RoomDefaults.defaultPowerLevels(
Matrix.of(context).client.userID!,
),
if (widget.spaceId != null)
StateEvent(
type: EventTypes.RoomJoinRules,
content: {
'join_rule': 'knock_restricted',
'allow': [
{
"type": "m.room_membership",
"room_id": widget.spaceId,
}
],
},
),
// Pangea#
],
// #Pangea
@ -164,7 +175,7 @@ class NewGroupController extends State<NewGroup> {
if (widget.spaceId != null) {
try {
final space = client.getRoomById(widget.spaceId!);
await space?.pangeaSetSpaceChild(room.id);
await space?.addToSpace(room.id);
} catch (err) {
ErrorHandler.logError(
e: "Failed to add room to space",
@ -187,7 +198,7 @@ class NewGroupController extends State<NewGroup> {
);
}
}
context.go('/rooms/$roomId/invite?filter=groups');
context.go('/rooms/$roomId/invite');
// Pangea#
}
@ -231,7 +242,7 @@ class NewGroupController extends State<NewGroup> {
final room = Matrix.of(context).client.getRoomById(spaceId);
if (room == null) return;
final spaceCode = room.classCode(context);
final spaceCode = room.classCode;
if (spaceCode != null) {
GoogleAnalytics.createClass(room.name, spaceCode);
}

View file

@ -65,27 +65,26 @@ class NewGroupView extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<CreateGroupType>(
selected: {controller.createGroupType},
onSelectionChanged: controller.setCreateGroupType,
segments: [
ButtonSegment(
value: CreateGroupType.group,
// #Pangea
// label: Text(L10n.of(context).group),
label: Text(L10n.of(context).chat),
// Pangea#
),
ButtonSegment(
value: CreateGroupType.space,
label: Text(L10n.of(context).space),
),
],
),
),
const SizedBox(height: 16),
// #Pangea
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: SegmentedButton<CreateGroupType>(
// selected: {controller.createGroupType},
// onSelectionChanged: controller.setCreateGroupType,
// segments: [
// ButtonSegment(
// value: CreateGroupType.group,
// label: Text(L10n.of(context).group),
// ),
// ButtonSegment(
// value: CreateGroupType.space,
// label: Text(L10n.of(context).space),
// ),
// ],
// ),
// ),
// const SizedBox(height: 16),
// Pangea#
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: controller.loading ? null : controller.selectPhoto,

View file

@ -1,16 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -33,13 +32,15 @@ class NewPrivateChatView extends StatelessWidget {
leading: const Center(child: BackButton()),
title: Text(L10n.of(context).newChat),
backgroundColor: theme.scaffoldBackgroundColor,
actions: [
TextButton(
onPressed:
UrlLauncher(context, AppConfig.startChatTutorial).launchUrl,
child: Text(L10n.of(context).help),
),
],
// #Pangea
// actions: [
// TextButton(
// onPressed:
// UrlLauncher(context, AppConfig.startChatTutorial).launchUrl,
// child: Text(L10n.of(context).help),
// ),
// ],
// Pangea#
),
body: MaxWidthBody(
withScrolling: false,
@ -138,15 +139,17 @@ class NewPrivateChatView extends StatelessWidget {
title: Text(L10n.of(context).shareInviteLink),
onTap: controller.inviteAction,
),
ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.tertiaryContainer,
foregroundColor: theme.colorScheme.onTertiaryContainer,
child: const Icon(Icons.group_add_outlined),
),
title: Text(L10n.of(context).createGroup),
onTap: () => context.go('/rooms/newgroup'),
),
// #Pangea
// ListTile(
// leading: CircleAvatar(
// backgroundColor: theme.colorScheme.tertiaryContainer,
// foregroundColor: theme.colorScheme.onTertiaryContainer,
// child: const Icon(Icons.group_add_outlined),
// ),
// title: Text(L10n.of(context).createGroup),
// onTap: () => context.go('/rooms/newgroup'),
// ),
// Pangea#
if (PlatformInfos.isMobile)
ListTile(
leading: CircleAvatar(
@ -181,7 +184,10 @@ class NewPrivateChatView extends StatelessWidget {
constraints:
const BoxConstraints(maxWidth: 256),
child: PrettyQrView.data(
data: 'https://matrix.to/#/$userId',
// #Pangea
// data: 'https://matrix.to/#/$userId',
data: Environment.frontendURL,
// Pangea#
decoration: PrettyQrDecoration(
shape: PrettyQrSmoothSymbol(
roundFactor: 1,

View file

@ -179,12 +179,8 @@ 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!,
),
RoomDefaults.defaultPowerLevels(
Matrix.of(context).client.userID!,
),
if (avatar != null && avatarUrl != null)
StateEvent(

View file

@ -1,60 +1,78 @@
Map<String, dynamic> defaultPowerLevels(String userID) => {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},
"events_default": 0,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
};
import 'package:matrix/matrix.dart';
Map<String, dynamic> restrictedPowerLevels(String userID) => {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},
"events_default": 50,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
};
class RoomDefaults {
static StateEvent defaultPowerLevels(String userID) => StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},
"events_default": 0,
"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,
},
};
static StateEvent restrictedPowerLevels(String userID) => StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.power_levels": 100,
"m.room.pinned_events": 50,
},
"events_default": 50,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
},
);
static StateEvent defaultSpacePowerLevels(String userID) => StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: {
"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,
},
},
);
static Visibility spaceChildVisibility = Visibility.private;
}

View file

@ -54,7 +54,7 @@ void chatListHandleSpaceTap(
if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) {
autoJoin(space);
} else if (justInputtedCode != null &&
justInputtedCode == space.classCode(context)) {
justInputtedCode == space.classCode) {
// do nothing
} else {
controller.showInviteDialog(space);

View file

@ -0,0 +1,221 @@
import 'package:flutter/material.dart' hide Visibility;
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
class PangeaChatAccessSettingsPageView extends StatelessWidget {
final ChatAccessSettingsController controller;
const PangeaChatAccessSettingsPageView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final room = controller.room;
return Scaffold(
appBar: AppBar(
leading: const Center(child: BackButton()),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.shield_outlined),
const SizedBox(width: 8),
Text(L10n.of(context).access),
],
),
),
body: MaxWidthBody(
showBorder: false,
child: StreamBuilder<Object>(
stream: room.client.onRoomState.stream
.where((update) => update.roomId == controller.room.id),
builder: (context, snapshot) {
return Container(
width: 400.0,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: FutureBuilder(
future: room.client.getRoomVisibilityOnDirectory(room.id),
builder: (context, snapshot) {
return Column(
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
ChatAccessTitle(
icon: Icons.search_outlined,
title: L10n.of(context).howSpaceCanBeFound,
),
ChatAccessTile(
emoji: "🏡",
title: L10n.of(context).private,
description: L10n.of(context).cannotBeFoundInSearch,
selected: snapshot.data == Visibility.private,
onTap: () {
if (snapshot.data == Visibility.private) return;
controller.setChatVisibilityOnDirectory(false);
},
),
ChatAccessTile(
emoji: "🌏",
title: L10n.of(context).public,
description: L10n.of(context).visibleToCommunity,
selected: snapshot.data == Visibility.public,
onTap: () {
if (snapshot.data == Visibility.public) return;
controller.setChatVisibilityOnDirectory(true);
},
),
const SizedBox(height: 8.0),
ChatAccessTitle(
icon: Icons.key_outlined,
title: L10n.of(context).howSpaceCanBeJoined,
),
ChatAccessTile(
emoji: "🤝",
title: L10n.of(context).restricted,
descriptionWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(L10n.of(context).canBeFoundVia),
Text(L10n.of(context).canBeFoundViaInvitation),
Text(L10n.of(context).canBeFoundViaCodeOrLink),
Text(L10n.of(context).canBeFoundViaKnock),
],
),
selected: room.joinRules == JoinRules.knock,
onTap: () => controller.setJoinRule(JoinRules.knock),
),
ChatAccessTile(
emoji: "👐",
title: L10n.of(context).open,
description: L10n.of(context).anyoneCanJoin,
selected: room.joinRules == JoinRules.public,
onTap: () => controller.setJoinRule(JoinRules.public),
),
],
);
},
),
);
},
),
),
);
}
}
class ChatAccessTitle extends StatelessWidget {
final IconData icon;
final String title;
const ChatAccessTitle({
super.key,
required this.icon,
required this.title,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: isColumnMode ? 32.0 : 24.0,
),
SizedBox(width: isColumnMode ? 32.0 : 16.0),
Text(
title,
style: isColumnMode
? theme.textTheme.titleLarge
: theme.textTheme.titleMedium,
),
],
);
}
}
class ChatAccessTile extends StatelessWidget {
final String emoji;
final String title;
final String? description;
final Widget? descriptionWidget;
final bool selected;
final VoidCallback? onTap;
const ChatAccessTile({
super.key,
required this.emoji,
required this.title,
this.description,
this.descriptionWidget,
this.selected = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Opacity(
opacity: selected ? 1.0 : 0.5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: Border.all(
color: selected
? theme.colorScheme.primaryContainer
: theme.colorScheme.outline,
width: 2,
),
color: selected
? theme.colorScheme.primaryContainer.withAlpha(50)
: null,
),
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Text(
emoji,
style: isColumnMode
? theme.textTheme.displayMedium
: theme.textTheme.displaySmall,
),
const SizedBox(width: 16.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: isColumnMode
? theme.textTheme.titleLarge
: theme.textTheme.titleMedium,
),
description != null
? Text(description!)
: descriptionWidget != null
? descriptionWidget!
: const SizedBox.shrink(),
],
),
),
],
),
),
),
);
}
}

View file

@ -330,7 +330,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
title: l10n.access,
icon: const Icon(Icons.shield_outlined),
onPressed: () => context.go('/rooms/${room.id}/details/access'),
visible: room.isSpace,
visible: room.isSpace && room.spaceParents.isEmpty,
enabled: room.isSpace && room.isRoomAdmin,
),
ButtonDetails(
@ -364,7 +364,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
icon: const Icon(Icons.add_outlined),
onPressed: widget.controller.addSubspace,
visible: room.isSpace &&
room.canSendEvent(
room.canChangeStateEvent(
EventTypes.SpaceChild,
),
showInMainView: false,

View file

@ -50,7 +50,7 @@ class SpaceInviteButtonsController extends State<SpaceInviteButtons> {
@override
Widget build(BuildContext context) {
final spaceCode = widget.room.classCode(context);
final spaceCode = widget.room.classCode;
if (!widget.room.isSpace || spaceCode == null) {
return const SizedBox.shrink();
}

View file

@ -1,149 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class VisibilityToggle extends StatefulWidget {
final Room room;
final Color? iconColor;
final Future<void> Function(matrix.Visibility) setVisibility;
final Future<void> Function(JoinRules) setJoinRules;
const VisibilityToggle({
required this.setVisibility,
required this.setJoinRules,
required this.room,
this.iconColor,
super.key,
});
@override
State<VisibilityToggle> createState() => VisibilityToggleState();
}
class VisibilityToggleState extends State<VisibilityToggle> {
Room get room => widget.room;
bool get _isPublic => room.joinRules == matrix.JoinRules.public;
matrix.Visibility? _visibility;
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_getVisibility();
}
Future<void> _getVisibility() async {
try {
final resp = await Matrix.of(context).client.getRoomVisibilityOnDirectory(
room.id,
);
_visibility = resp ?? matrix.Visibility.private;
} catch (e) {
_error = e.toString();
} finally {
setState(() {
_loading = false;
});
}
}
Future<void> _setVisibility(matrix.Visibility visibility) async {
try {
await widget.setVisibility(visibility);
_visibility = visibility;
} catch (e) {
_error = e.toString();
} finally {
if (mounted) {
setState(() {});
}
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile.adaptive(
activeColor: AppConfig.activeToggleColor,
title: Text(
L10n.of(context).requireCodeToJoin,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
secondary: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: widget.iconColor,
child: const Icon(Icons.key_outlined),
),
value: !_isPublic,
onChanged: (value) =>
widget.setJoinRules(value ? JoinRules.knock : JoinRules.public),
),
ListTile(
title: Text(
L10n.of(context).canFindInSearch,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
leading: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: widget.iconColor,
child: const Icon(Icons.search_outlined),
),
onTap: _visibility != null
? () => showFutureLoadingDialog(
future: () async {
_setVisibility(
_visibility == matrix.Visibility.public
? matrix.Visibility.private
: matrix.Visibility.public,
);
},
context: context,
)
: null,
trailing: _loading || _error != null
? SizedBox(
height: 24.0,
width: 24.0,
child: _error != null
? Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
)
: const CircularProgressIndicator.adaptive(),
)
: Switch.adaptive(
activeColor: AppConfig.activeToggleColor,
value: _visibility == matrix.Visibility.public,
onChanged: (value) => showFutureLoadingDialog(
future: () async {
_setVisibility(
value
? matrix.Visibility.public
: matrix.Visibility.private,
);
},
context: context,
),
),
),
],
);
}
}

View file

@ -269,12 +269,8 @@ class PangeaController {
preset: CreateRoomPreset.trustedPrivateChat,
initialState: [
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(
matrixState.client.userID!,
),
RoomDefaults.defaultPowerLevels(
matrixState.client.userID!,
),
],
);

View file

@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:html_unescape/html_unescape.dart';
import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/markdown.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

View file

@ -15,7 +15,7 @@ extension ChildrenAndParentsRoomExtension on Room {
/// Wrapper around call to setSpaceChild with added functionality
/// to prevent adding one room to multiple spaces, and resets the
/// subspace's JoinRules and Visibility to defaults.
Future<void> pangeaSetSpaceChild(
Future<void> addToSpace(
String roomId, {
bool? suggested,
}) async {
@ -38,11 +38,9 @@ extension ChildrenAndParentsRoomExtension on Room {
}
try {
await setSpaceChild(roomId, suggested: suggested);
await child.setJoinRules(JoinRules.public);
await child.client.setRoomVisibilityOnDirectory(
await _trySetSpaceChild(
roomId,
visibility: matrix.Visibility.private,
suggested: suggested,
);
} catch (err, stack) {
ErrorHandler.logError(
@ -57,6 +55,31 @@ extension ChildrenAndParentsRoomExtension on Room {
}
}
Future<void> _trySetSpaceChild(
String roomId, {
bool? suggested,
int retries = 0,
}) async {
final Room? child = client.getRoomById(roomId);
if (child == null) return;
try {
await setSpaceChild(roomId, suggested: suggested);
} catch (err) {
retries++;
if (retries < 3) {
await Future.delayed(const Duration(seconds: 1));
return _trySetSpaceChild(
roomId,
suggested: suggested,
retries: retries,
);
} else {
rethrow;
}
}
}
/// A map of child suggestion status for a space.
Map<String, bool> get spaceChildSuggestionStatus {
if (!isSpace) return {};

View file

@ -1,15 +1,8 @@
part of "pangea_room_extension.dart";
extension SpaceRoomExtension on Room {
String? classCode(BuildContext context) {
if (!isSpace) {
for (final Room potentialClassRoom in pangeaSpaceParents) {
if (potentialClassRoom.isSpace) {
return SpaceRoomExtension(potentialClassRoom).classCode(context);
}
}
return null;
}
String? get classCode {
if (!isSpace) return null;
final roomJoinRules = getState(EventTypes.RoomJoinRules, "");
if (roomJoinRules != null) {
final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode);

View file

@ -122,10 +122,18 @@ extension SpacesClientExtension on Client {
type: EventTypes.RoomAvatar,
content: {'url': introChatUploadURL.toString()},
),
RoomDefaults.defaultPowerLevels(userID!),
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(userID!),
type: EventTypes.RoomJoinRules,
content: {
'join_rule': 'knock_restricted',
'allow': [
{
"type": "m.room_membership",
"room_id": space.id,
}
],
},
),
],
);
@ -142,10 +150,18 @@ extension SpacesClientExtension on Client {
type: EventTypes.RoomAvatar,
content: {'url': announcementsChatUploadURL.toString()},
),
RoomDefaults.restrictedPowerLevels(userID!),
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: restrictedPowerLevels(userID!),
type: EventTypes.RoomJoinRules,
content: {
'join_rule': 'knock_restricted',
'allow': [
{
"type": "m.room_membership",
"room_id": space.id,
}
],
},
),
],
);
@ -166,11 +182,11 @@ extension SpacesClientExtension on Client {
}
}
final addIntroChatFuture = space.pangeaSetSpaceChild(
final addIntroChatFuture = space.addToSpace(
roomIds[0],
);
final addAnnouncementsChatFuture = space.pangeaSetSpaceChild(
final addAnnouncementsChatFuture = space.addToSpace(
roomIds[1],
);
@ -186,11 +202,7 @@ extension SpacesClientExtension on Client {
required JoinRules joinRules,
}) {
return [
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultSpacePowerLevels(userID),
),
RoomDefaults.defaultSpacePowerLevels(userID),
StateEvent(
type: EventTypes.RoomJoinRules,
content: {
@ -200,4 +212,69 @@ extension SpacesClientExtension on Client {
),
];
}
/// Keep the room's current join rule state event content (except for what's intentionally replaced)
/// since space's access codes were stored there. Don't want to accidentally remove them.
Future<void> pangeaSetJoinRules(
String roomId,
String joinRule, {
List<Map<String, dynamic>>? allow,
}) async {
final room = getRoomById(roomId);
if (room == null) {
throw Exception('Room not found for user ID: $userID');
}
final currentJoinRule = room
.getState(
EventTypes.RoomJoinRules,
)
?.content ??
{};
if (currentJoinRule[ModelKey.joinRule] == joinRule &&
(currentJoinRule['allow'] == allow)) {
return; // No change needed
}
currentJoinRule[ModelKey.joinRule] = joinRule;
currentJoinRule['allow'] = allow;
await setRoomStateWithKey(
roomId,
EventTypes.RoomJoinRules,
'',
currentJoinRule,
);
}
Future<void> setSpaceChildAccess(String roomId) async {
await pangeaSetJoinRules(
roomId,
'knock_restricted',
allow: [
{
"type": "m.room_membership",
"room_id": id,
}
],
);
await setRoomVisibilityOnDirectory(
roomId,
visibility: Visibility.private,
);
}
Future<void> resetSpaceChildAccess(String roomId) async {
await pangeaSetJoinRules(
roomId,
JoinRules.knock.toString().replaceAll('JoinRules.', ''),
);
await setRoomVisibilityOnDirectory(
roomId,
visibility: Visibility.private,
);
}
}

View file

@ -1,141 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
class AddRoomDialog extends StatefulWidget {
const AddRoomDialog({
super.key,
});
@override
AddRoomDialogState createState() => AddRoomDialogState();
}
class AddRoomDialogState extends State<AddRoomDialog> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _roomNameController = TextEditingController();
final TextEditingController _roomDescriptionController =
TextEditingController();
@override
void dispose() {
_roomNameController.dispose();
_roomDescriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Form(
key: _formKey,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
style: Theme.of(context).textTheme.headlineSmall,
L10n.of(context).createChat,
),
const SizedBox(height: 20),
TextFormField(
controller: _roomNameController,
decoration: InputDecoration(
hintText: L10n.of(context).chatName,
),
minLines: 1,
maxLines: 1,
maxLength: 64,
validator: (text) {
if (text == null || text.isEmpty) {
return L10n.of(context).pleaseChoose;
}
return null;
},
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 20),
TextFormField(
controller: _roomDescriptionController,
decoration: InputDecoration(
hintText: L10n.of(context).chatDescription,
),
minLines: 4,
maxLines: 8,
maxLength: 255,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.of(context).pop(null);
},
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 20),
TextButton(
onPressed: () async {
final isValid = _formKey.currentState!.validate();
if (!isValid) return;
Navigator.of(context).pop(
RoomResponse(
roomName: _roomNameController.text,
roomDescription: _roomDescriptionController.text,
joinRules: JoinRules.public,
visibility: matrix.Visibility.private,
),
);
},
child: Text(L10n.of(context).confirm),
),
],
),
),
],
),
),
),
);
}
}
class RoomResponse {
final String roomName;
final String roomDescription;
final JoinRules joinRules;
final matrix.Visibility visibility;
RoomResponse({
required this.roomName,
required this.roomDescription,
required this.joinRules,
required this.visibility,
});
Map<String, dynamic> toJson() {
return {
'roomName': roomName,
'roomDescripion': roomDescription,
'joinRules': joinRules,
'visibility': visibility,
};
}
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:share_plus/share_plus.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import '../widgets/matrix.dart';
@ -34,10 +35,13 @@ abstract class FluffyShare {
final client = Matrix.of(context).client;
final ownProfile = await client.fetchOwnProfile();
await FluffyShare.share(
L10n.of(context).inviteText(
ownProfile.displayName ?? client.userID!,
'https://matrix.to/#/${client.userID}?client=im.fluffychat',
),
// #Pangea
// L10n.of(context).inviteText(
// ownProfile.displayName ?? client.userID!,
// 'https://matrix.to/#/${client.userID}?client=im.fluffychat',
// ),
"${ownProfile.displayName ?? client.userID!} invited you to Pangea Chat.\nOpen the invite link: \n ${Environment.frontendURL}",
// Pangea#
context,
);
}

View file

@ -183,7 +183,9 @@ class UserDialog extends StatelessWidget {
bigButtons: true,
onPressed: () async {
final router = GoRouter.of(context);
Navigator.of(context).pop();
// #Pangea
// Navigator.of(context).pop();
// Pangea#
final roomIdResult = await showFutureLoadingDialog(
context: context,
// #Pangea
@ -194,6 +196,9 @@ class UserDialog extends StatelessWidget {
),
// Pangea#
);
// #Pangea
Navigator.of(context).pop();
// Pangea#
final roomId = roomIdResult.result;
if (roomId == null) return;
router.go('/rooms/$roomId');

View file

@ -22,7 +22,7 @@ Future<Result<T>> showFutureLoadingDialog<T>({
ExceptionContext? exceptionContext,
bool ignoreError = false,
// #Pangea
String? Function(Object, StackTrace?)? onError,
Object? Function(Object, StackTrace?)? onError,
String? Function()? onSuccess,
VoidCallback? onDismiss,
// Pangea#
@ -82,7 +82,7 @@ class LoadingDialog<T> extends StatefulWidget {
final Future<T> future;
final ExceptionContext? exceptionContext;
// #Pangea
final String? Function(Object, StackTrace?)? onError;
final Object? Function(Object, StackTrace?)? onError;
final String? Function()? onSuccess;
final VoidCallback? onDismiss;
// Pangea#