import "dart:io"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_ui/material_ui.dart"; import "package:intl/intl.dart"; import "package:m3e_buttons/m3e_buttons.dart"; import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/init_complete.dart"; import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/unified_push.dart"; import "package:nexus/controllers/spec_versions.dart"; import "package:nexus/controllers/unified_push_allowed.dart"; import "package:nexus/controllers/unified_push_distributors.dart"; import "package:nexus/helpers/extensions/distributor_name.dart"; import "package:nexus/models/account_data.dart"; import "package:nexus/models/room_order.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/settings/dialog_list_tile.dart"; import "package:nexus/widgets/reset_encryption_sheet.dart"; import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; class SettingsSectionsController extends AsyncNotifier>> { @override Future>> build() async { final settings = await ref.watch(SettingsController.provider.future); return .new({ "General": .new([ .new( title: "Appearance", icon: Icons.brush, settings: .new([ .new( title: "Theme", description: "Toggle between Light Mode, Dark Mode, and System themes.", icon: Icons.contrast, builder: (title, description, icon) => DialogListTile( icon: Icon(icon), title: title, subtitle: Text(description), value: settings.theme, options: ThemeMode.values, getName: (option) => toBeginningOfSentenceCase(option.name), onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(theme: value)) .onError(showError), ), ), .new( title: "Use Dynamic Theme", icon: Icons.palette, description: "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.useDynamicTheming, onChanged: (Platform.isAndroid || Platform.isLinux || Platform.isMacOS || Platform.isWindows) ? (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(useDynamicTheming: value)) .onError(showError) : null, ), ), ]), ), .new( title: "Behavior", icon: Icons.psychology, settings: .new([ .new( title: "All Rooms In Home", description: "Show all rooms in home, instead of just ones that aren't in spaces", icon: Icons.home, builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.showAllInHome, onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(showAllInHome: value)) .onError(showError), ), ), .new( title: "Room Ordering", description: "Sort rooms by recent activity, optionally showing mentioned and unread rooms first.", icon: Icons.sort, builder: (title, description, icon) => DialogListTile( icon: Icon(icon), title: title, subtitle: Text(description), value: settings.roomOrder, options: RoomOrder.values, getName: (option) => switch (option) { .unreadFirst => "Mentioned, then unread", .recentActivity => "Recent activity only", }, onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(roomOrder: value)) .onError(showError), ), ), .new( title: "Swipe To Reply", description: "Swipe a message to the right to reply to it.", icon: Icons.swipe_right, builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.swipeToReply, onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(swipeToReply: value)) .onError(showError), ), ), if (!Platform.isAndroid && !Platform.isIOS) .new( title: "Show Minimize Button", description: "Enable this to add a minimize button to the appbar", icon: Icons.minimize, builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.showMinimizeButton, onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(showMinimizeButton: value)) .onError(showError), ), ), .new( title: "Linux Mobile Mode", description: "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", icon: Icons.construction, builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.linuxMobileMode, onChanged: Platform.isLinux ? (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(linuxMobileMode: value)) .onError(showError) : null, ), ), .new( title: "Hide Membership Events", description: "Hides membership events (invites, joins, leaves, bans & knocks) from appearing in conversations.", icon: Icons.person_off, builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), secondary: Icon(icon), value: settings.hideMembershipEvents, onChanged: (value) => ref .read(SettingsController.provider.notifier) .set(settings.copyWith(hideMembershipEvents: value)) .onError(showError), ), ), ]), ), ]), if (ref.watch(ClientStateController.provider)?.isLoggedIn == true) "Account": .new([ .new(title: "Profile", icon: Icons.person, settings: .new([])), .new( title: "Notifications", icon: Icons.notifications, settings: .new([ .new( title: "Push notifications", description: "Enable push notifications using Web Push, and choose which UnifiedPush distributor to use.", builder: (title, description, icon) => HookConsumer( builder: (context, ref, _) { final loading = useState(false); final pusherRegistered = ref.watch( UnifiedPushController.provider, ); final unifiedPushAllowed = ref.watch( UnifiedPushAllowedController.provider, ); final distributors = ref.watch( UnifiedPushDistributorsController.provider, ); final current = pusherRegistered.value; return DialogListTile( title: title, subtitle: Text( unifiedPushAllowed.maybeWhen( data: (data) => data, orElse: () => null, ) ?? description, ), icon: Icon(icon), value: current, options: [ null, ...?distributors.value, if (current != null && distributors.value?.contains(current) != true) current, ], getName: (option) => option == null ? "Off" : option.distributorName, onChanged: loading.value ? null : unifiedPushAllowed.maybeWhen( data: (data) => data == null, orElse: () => false, ) ? pusherRegistered.maybeWhen( data: (_) => (value) async { if (value == current) return; try { loading.value = true; if (value != null) { final messenger = ScaffoldMessenger.of( context, ); if (!await ref .read( NotificationController .provider .notifier, ) .requestPermissions()) { messenger.showSnackBar( .new( content: Text( "Notifications are disabled. Enable them for Nexus in your system settings.", ), ), ); return; } await ref .read( UnifiedPushController .provider .notifier, ) .register(value); } else { await ref .read( UnifiedPushController .provider .notifier, ) .deregister(); } } catch (error, stackTrace) { showError(error, stackTrace); } finally { loading.value = false; } }, orElse: () => null, ) : null, ); }, ), icon: Icons.notification_add, ), ]), ), .new( title: "Safety", icon: Icons.gpp_good, settings: .new([ .new( title: "Invite Blocking", description: "Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).", builder: (title, description, icon) => Consumer( builder: (context, ref, _) { final specVersionsResponse = ref.watch( SpecVersionsController.provider, ); return DialogListTile( icon: Icon(icon), title: title, subtitle: Text(description), value: ref .watch(AccountDataController.provider) .invitePermissionConfig .defaultAction, options: specVersionsResponse.maybeWhen( data: (data) => data.unstableFeatures.msc4494, orElse: () => true, ) ? DefaultInviteAction.values : IList(DefaultInviteAction.values) .remove(.denyPublic) .toList(), getName: (option) => switch (option) { .allow => "Allow", .deny => "Deny", .denyPublic => "Deny public", }, onChanged: specVersionsResponse.maybeWhen( data: (_) => (value) => ref .read(ClientController.provider.notifier) .setAccountData( .new( type: AccountData.invitePermissionConfigKey, content: InvitePermissionConfig( defaultAction: value, ), ), ) .onError(showError), orElse: () => null, ), ); }, ), icon: Icons.person_off, ), ]), ), .new( title: "Other", icon: Icons.key, settings: .new([ .new( title: "Reset Encryption Identity", description: "Replace your encryption identity and recovery key with new ones, if you've lost your recovery key or your encryption is broken.", builder: (title, description, icon) => Builder( builder: (context) => M3EButton.icon( onPressed: () => context.showAdaptiveSheet( (_) => const ResetEncryptionSheet(), ), label: Text(title), icon: Icon(icon), tooltip: description, ), ), icon: Icons.key_off, ), .new( title: "Log Out", description: "Log out of your account, returning you to the login page.", builder: (title, description, icon) => Builder( builder: (context) { final colorScheme = Theme.of(context).colorScheme; return M3EButton.icon( onPressed: () async { final container = ProviderScope.containerOf(context); final route = ModalRoute.of(context)!; Navigator.of(context).popUntil((r) => r.isFirst); await route.completed; await container .read(UnifiedPushController.provider.notifier) .deregister() .onError(showError); container.invalidate(InitCompleteController.provider); await container .read(ClientController.provider.notifier) .logout() .onError(showError); }, label: Text(title), icon: Icon(icon), tooltip: description, decoration: .styleFrom( backgroundColor: colorScheme.errorContainer, foregroundColor: colorScheme.onErrorContainer, ), ); }, ), icon: Icons.logout, ), ]), ), ]), }); } static final provider = AsyncNotifierProvider.autoDispose< SettingsSectionsController, IMap> >(SettingsSectionsController.new); }