diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart index 7bf0d35..72ca2d2 100644 --- a/lib/helpers/extensions/build_event_options.dart +++ b/lib/helpers/extensions/build_event_options.dart @@ -41,7 +41,7 @@ extension BuildEventOptions on Event { .read(PinnedIdsController.provider(roomId)) .contains(eventId); - void showReasonDialog({ + void showReasonSheet({ required String title, required String description, required String action, @@ -235,7 +235,7 @@ extension BuildEventOptions on Event { ) && !eventId.isFake) PopupMenuItem( - onTap: () => showReasonDialog( + onTap: () => showReasonSheet( title: "Delete Message", description: "Are you sure you want to delete this message? " @@ -260,7 +260,7 @@ extension BuildEventOptions on Event { ), PopupMenuItem( - onTap: () => showReasonDialog( + onTap: () => showReasonSheet( title: "Report", description: "Report this event to your server administrators, " diff --git a/lib/helpers/extensions/get_localpart.dart b/lib/helpers/extensions/get_localpart.dart index 729d921..71e9808 100644 --- a/lib/helpers/extensions/get_localpart.dart +++ b/lib/helpers/extensions/get_localpart.dart @@ -1,6 +1,9 @@ extension GetLocalpart on String { String get localpart => length > 1 ? substring(1).split(":").first : "?"; + /// Whether this looks like a valid Matrix user ID, e.g. `@user:example.com`. + bool get isValidUserId => RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(this); + String? get serverName { final index = indexOf(":"); return index == -1 ? null : substring(index + 1); diff --git a/lib/helpers/extensions/show_about_dialog.dart b/lib/helpers/extensions/show_about_sheet.dart similarity index 97% rename from lib/helpers/extensions/show_about_dialog.dart rename to lib/helpers/extensions/show_about_sheet.dart index 689795a..ee49283 100644 --- a/lib/helpers/extensions/show_about_dialog.dart +++ b/lib/helpers/extensions/show_about_sheet.dart @@ -7,8 +7,8 @@ import "package:m3e_card_list/m3e_card_list.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:package_info_plus/package_info_plus.dart"; -extension ShowAboutDialog on BuildContext { - Future showAboutDialog(WidgetRef ref) async { +extension ShowAboutSheet on BuildContext { + Future showAboutSheet(WidgetRef ref) async { final packageInfo = await PackageInfo.fromPlatform(); if (mounted) { diff --git a/lib/helpers/extensions/show_error_snack_bar.dart b/lib/helpers/extensions/show_error_snack_bar.dart new file mode 100644 index 0000000..4af01c5 --- /dev/null +++ b/lib/helpers/extensions/show_error_snack_bar.dart @@ -0,0 +1,28 @@ +import "package:material_ui/material_ui.dart"; + +extension ShowErrorSnackBar on ScaffoldMessengerState { + /// Shows [message] in a snackbar styled as an error. + ScaffoldFeatureController showErrorSnackBar( + String message, { + ({String label, VoidCallback onPressed})? action, + }) { + final colorScheme = Theme.of(context).colorScheme; + + return showSnackBar( + .new( + backgroundColor: colorScheme.errorContainer, + content: Text( + message, + style: .new(color: colorScheme.onErrorContainer), + ), + action: action == null + ? null + : .new( + label: action.label, + onPressed: action.onPressed, + textColor: colorScheme.onErrorContainer, + ), + ), + ); + } +} diff --git a/lib/helpers/extensions/with_progress_snackbar.dart b/lib/helpers/extensions/with_progress_snackbar.dart index cd8e494..c6d8fa0 100644 --- a/lib/helpers/extensions/with_progress_snackbar.dart +++ b/lib/helpers/extensions/with_progress_snackbar.dart @@ -1,4 +1,5 @@ import "package:material_ui/material_ui.dart"; +import "package:nexus/helpers/extensions/show_error_snack_bar.dart"; extension WithProgressSnackbar on BuildContext { /// Shows [progress] in a snackbar while [action] runs, and shows any error @@ -10,8 +11,6 @@ extension WithProgressSnackbar on BuildContext { Future Function() action, ) async { final messenger = ScaffoldMessenger.of(this); - final colorScheme = Theme.of(this).colorScheme; - final snackbar = messenger.showSnackBar( .new(content: Text(progress), duration: const .new(days: 1)), ); @@ -19,15 +18,7 @@ extension WithProgressSnackbar on BuildContext { try { return await action(); } catch (error) { - messenger.showSnackBar( - .new( - backgroundColor: colorScheme.errorContainer, - content: Text( - error.toString(), - style: .new(color: colorScheme.onErrorContainer), - ), - ), - ); + messenger.showErrorSnackBar(error.toString()); return null; } finally { diff --git a/lib/widgets/pages/select_server.dart b/lib/widgets/pages/select_server.dart index f9f6498..66c40a5 100644 --- a/lib/widgets/pages/select_server.dart +++ b/lib/widgets/pages/select_server.dart @@ -6,6 +6,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/auth_url.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_id.dart"; +import "package:nexus/helpers/extensions/show_error_snack_bar.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/main.dart"; import "package:nexus/models/gomuks_exception.dart"; @@ -27,14 +28,8 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { Future setHomeserver(Uri? newHomeserver) async { if (newHomeserver == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Failed to parse homeserver URL. Are you sure you typed it correctly?", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), + ScaffoldMessenger.of(context).showErrorSnackBar( + "Failed to parse homeserver URL. Are you sure you typed it correctly?", ); } isLoading.value = true; @@ -58,14 +53,8 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { } on GomuksException catch (error) { if (error.errorCode != "M_UNRECOGNIZED") rethrow; if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "${url.host} doesn't support OAuth login, which Nexus requires. Try a different homeserver, or ask your homeserver's admin to enable OAuth.", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), + ScaffoldMessenger.of(context).showErrorSnackBar( + "${url.host} doesn't support OAuth login, which Nexus requires. Try a different homeserver, or ask your homeserver's admin to enable OAuth.", ); } return; @@ -109,18 +98,11 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { } if (newUrl == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Homeserver verification failed.", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - action: SnackBarAction( - onPressed: () => tryLogin(newHomeserver!).onError(showError), - label: "Attempt log in anyways", - textColor: theme.colorScheme.onErrorContainer, - ), - backgroundColor: theme.colorScheme.errorContainer, + ScaffoldMessenger.of(context).showErrorSnackBar( + "Homeserver verification failed.", + action: ( + label: "Attempt log in anyways", + onPressed: () => tryLogin(newHomeserver!).onError(showError), ), ); } else { diff --git a/lib/widgets/pages/settings.dart b/lib/widgets/pages/settings.dart index 1760b89..2a813e5 100644 --- a/lib/widgets/pages/settings.dart +++ b/lib/widgets/pages/settings.dart @@ -7,7 +7,7 @@ import "package:m3e_card_list/m3e_card_list.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/controllers/settings_sections.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_about_dialog.dart"; +import "package:nexus/helpers/extensions/show_about_sheet.dart"; import "package:nexus/widgets/pages/settings_category.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; @@ -106,7 +106,7 @@ class const SettingsPage({super.key}) extends ConsumerWidget { actionsPadding: .symmetric(horizontal: 12), actions: [ IconButton( - onPressed: () => context.showAboutDialog(ref), + onPressed: () => context.showAboutSheet(ref), icon: Icon(Icons.info_outline), ), ], diff --git a/lib/widgets/room_menu_button.dart b/lib/widgets/room_menu_button.dart index a0647bf..b7b52de 100644 --- a/lib/widgets/room_menu_button.dart +++ b/lib/widgets/room_menu_button.dart @@ -1,5 +1,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; +import "package:nexus/helpers/extensions/get_localpart.dart"; +import "package:nexus/helpers/extensions/with_progress_snackbar.dart"; import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; import "package:nexus/widgets/sheet_layout.dart"; import "package:flutter/services.dart"; @@ -58,7 +60,7 @@ final class const RoomMenuButton( final userIdController = useTextEditingController(); final reasonController = useTextEditingController(); final text = useValueListenable(userIdController).text.trim(); - final valid = RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(text); + final valid = text.isValidUserId; void submit() { if (!valid) return; @@ -130,19 +132,12 @@ final class const RoomMenuButton( child: Text("Cancel"), ), TextButton( - onPressed: () async { + onPressed: () { + context.withProgressSnackbar( + "Leaving room...", + () => client.leaveRoom(room!.metadata!.id), + ); Navigator.of(context).pop(); - final snackbar = ScaffoldMessenger.of(context) - .showSnackBar( - .new( - content: Text("Leaving room..."), - duration: Duration(days: 1), - ), - ); - await client - .leaveRoom(room!.metadata!.id) - .onError(showError) - .whenComplete(snackbar.close); }, child: Text("Leave"), ), diff --git a/lib/widgets/start_dm_sheet.dart b/lib/widgets/start_dm_sheet.dart index 98bf042..4952b0a 100644 --- a/lib/widgets/start_dm_sheet.dart +++ b/lib/widgets/start_dm_sheet.dart @@ -1,5 +1,6 @@ import "package:flutter_hooks/flutter_hooks.dart"; import "package:material_ui/material_ui.dart"; +import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/create_room.dart"; import "package:nexus/widgets/sheet_layout.dart"; @@ -12,7 +13,7 @@ class const StartDmSheet({final String? userId, final String? name, super.key}) final userIdController = useTextEditingController(); final text = useValueListenable(userIdController).text.trim(); final target = userId ?? text; - final valid = RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(target); + final valid = target.isValidUserId; final encrypted = useState(true); final reasonController = useTextEditingController(); diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart index ad2c511..a648876 100644 --- a/lib/widgets/user_bottom_sheet.dart +++ b/lib/widgets/user_bottom_sheet.dart @@ -36,7 +36,7 @@ final class const UserBottomSheet( ); final client = ref.read(ClientController.provider.notifier); - void showMembershipDialog(MembershipAction action) => + void showMembershipSheet(MembershipAction action) => context.showAdaptiveSheet( (context) => HookBuilder( builder: (context) { @@ -229,7 +229,7 @@ final class const UserBottomSheet( children: [ if (member.status == .join || member.status == .invite) M3EButton.icon( - onPressed: () => showMembershipDialog(.kick), + onPressed: () => showMembershipSheet(.kick), shape: .square, icon: Icon(Icons.sports_martial_arts), label: Text("Kick"), @@ -244,7 +244,7 @@ final class const UserBottomSheet( ), M3EButton.icon( - onPressed: () => showMembershipDialog(.ban), + onPressed: () => showMembershipSheet(.ban), shape: .square, icon: Icon(Icons.gavel), label: Text("Ban"),