From e4a2c13a6fd83e343334dbeb10679fd96487e20c Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 5 Feb 2025 08:54:55 +0100 Subject: [PATCH] feat: Display all push rules and allow to enable disable them --- assets/l10n/intl_en.arb | 54 +++++- .../homeserver_picker_view.dart | 4 - .../push_rule_extensions.dart | 121 ++++++++++++++ .../settings_notifications.dart | 154 +++++------------- .../settings_notifications_view.dart | 71 ++++---- 5 files changed, 251 insertions(+), 153 deletions(-) create mode 100644 lib/pages/settings_notifications/push_rule_extensions.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a1fb4b72f..dfd1fa3fb 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2841,5 +2841,57 @@ "open": "Open", "waitingForServer": "Waiting for server...", "appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*.", - "newChatRequest": "📩 New chat request" + "newChatRequest": "📩 New chat request", + "contentNotificationSettings": "Content notification settings", + "generalNotificationSettings": "General notification settings", + "roomNotificationSettings": "Room notification settings", + "userSpecificNotificationSettings": "User specific notification settings", + "otherNotificationSettings": "Other notification settings", + "notificationRuleContainsUserName": "Contains User Name", + "notificationRuleContainsUserNameDescription": "Notifies the user when a message contains their username.", + "notificationRuleMaster": "Mute all notifications", + "notificationRuleMasterDescription": "Overrides all other rules and disables all notifications.", + "notificationRuleSuppressNotices": "Suppress Automated Messages", + "notificationRuleSuppressNoticesDescription": "Suppresses notifications from automated clients like bots.", + "notificationRuleInviteForMe": "Invite for Me", + "notificationRuleInviteForMeDescription": "Notifies the user when they are invited to a room.", + "notificationRuleMemberEvent": "Member Event", + "notificationRuleMemberEventDescription": "Suppresses notifications for membership events.", + "notificationRuleIsUserMention": "User Mention", + "notificationRuleIsUserMentionDescription": "Notifies the user when they are directly mentioned in a message.", + "notificationRuleContainsDisplayName": "Contains Display Name", + "notificationRuleContainsDisplayNameDescription": "Notifies the user when a message contains their display name.", + "notificationRuleIsRoomMention": "Room Mention", + "notificationRuleIsRoomMentionDescription": "Notifies the user when there is a room mention.", + "notificationRuleRoomnotif": "Room Notification", + "notificationRuleRoomnotifDescription": "Notifies the user when a message contains '@room'.", + "notificationRuleTombstone": "Tombstone", + "notificationRuleTombstoneDescription": "Notifies the user about room deactivation messages.", + "notificationRuleReaction": "Reaction", + "notificationRuleReactionDescription": "Suppresses notifications for reactions.", + "notificationRuleRoomServerAcl": "Room Server ACL", + "notificationRuleRoomServerAclDescription": "Suppresses notifications for room server access control lists (ACL).", + "notificationRuleSuppressEdits": "Suppress Edits", + "notificationRuleSuppressEditsDescription": "Suppresses notifications for edited messages.", + "notificationRuleCall": "Call", + "notificationRuleCallDescription": "Notifies the user about calls.", + "notificationRuleEncryptedRoomOneToOne": "Encrypted Room One-to-One", + "notificationRuleEncryptedRoomOneToOneDescription": "Notifies the user about messages in encrypted one-to-one rooms.", + "notificationRuleRoomOneToOne": "Room One-to-One", + "notificationRuleRoomOneToOneDescription": "Notifies the user about messages in one-to-one rooms.", + "notificationRuleMessage": "Message", + "notificationRuleMessageDescription": "Notifies the user about general messages.", + "notificationRuleEncrypted": "Encrypted", + "notificationRuleEncryptedDescription": "Notifies the user about messages in encrypted rooms.", + "notificationRuleJitsi": "Jitsi", + "notificationRuleJitsiDescription": "Notifies the user about Jitsi widget events.", + "notificationRuleServerAcl": "Suppress Server ACL Events", + "notificationRuleServerAclDescription": "Suppresses notifications for Server ACL events.", + "unknownPushRule": "Unknown push rule '{rule}'", + "@unknownPushRule": { + "type": "text", + "placeholders": { + "rule": {} + } + } } diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 8ae67077c..b1935abd6 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -122,10 +122,6 @@ class HomeserverPickerView extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 32.0), child: SelectableLinkify( text: L10n.of(context).appIntroduction, - style: TextStyle( - color: theme.colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w500, - ), textAlign: TextAlign.center, linkStyle: TextStyle( color: theme.colorScheme.secondary, diff --git a/lib/pages/settings_notifications/push_rule_extensions.dart b/lib/pages/settings_notifications/push_rule_extensions.dart new file mode 100644 index 000000000..59dd6997a --- /dev/null +++ b/lib/pages/settings_notifications/push_rule_extensions.dart @@ -0,0 +1,121 @@ +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +extension PushRuleExtension on PushRule { + String getPushRuleName(L10n l10n) { + switch (ruleId) { + case '.m.rule.contains_user_name': + return l10n.notificationRuleContainsUserName; + case '.m.rule.master': + return l10n.notificationRuleMaster; + case '.m.rule.suppress_notices': + return l10n.notificationRuleSuppressNotices; + case '.m.rule.invite_for_me': + return l10n.notificationRuleInviteForMe; + case '.m.rule.member_event': + return l10n.notificationRuleMemberEvent; + case '.m.rule.is_user_mention': + return l10n.notificationRuleIsUserMention; + case '.m.rule.contains_display_name': + return l10n.notificationRuleContainsDisplayName; + case '.m.rule.is_room_mention': + return l10n.notificationRuleIsRoomMention; + case '.m.rule.roomnotif': + return l10n.notificationRuleRoomnotif; + case '.m.rule.tombstone': + return l10n.notificationRuleTombstone; + case '.m.rule.reaction': + return l10n.notificationRuleReaction; + case '.m.rule.room_server_acl': + return l10n.notificationRuleRoomServerAcl; + case '.m.rule.suppress_edits': + return l10n.notificationRuleSuppressEdits; + case '.m.rule.call': + return l10n.notificationRuleCall; + case '.m.rule.encrypted_room_one_to_one': + return l10n.notificationRuleEncryptedRoomOneToOne; + case '.m.rule.room_one_to_one': + return l10n.notificationRuleRoomOneToOne; + case '.m.rule.message': + return l10n.notificationRuleMessage; + case '.m.rule.encrypted': + return l10n.notificationRuleEncrypted; + case '.m.rule.room.server_acl': + return l10n.notificationRuleServerAcl; + case '.im.vector.jitsi': + return l10n.notificationRuleJitsi; + default: + return ruleId.split('.').last.replaceAll('_', ' ').capitalize(); + } + } + + String getPushRuleDescription(L10n l10n) { + switch (ruleId) { + case '.m.rule.contains_user_name': + return l10n.notificationRuleContainsUserNameDescription; + case '.m.rule.master': + return l10n.notificationRuleMasterDescription; + case '.m.rule.suppress_notices': + return l10n.notificationRuleSuppressNoticesDescription; + case '.m.rule.invite_for_me': + return l10n.notificationRuleInviteForMeDescription; + case '.m.rule.member_event': + return l10n.notificationRuleMemberEventDescription; + case '.m.rule.is_user_mention': + return l10n.notificationRuleIsUserMentionDescription; + case '.m.rule.contains_display_name': + return l10n.notificationRuleContainsDisplayNameDescription; + case '.m.rule.is_room_mention': + return l10n.notificationRuleIsRoomMentionDescription; + case '.m.rule.roomnotif': + return l10n.notificationRuleRoomnotifDescription; + case '.m.rule.tombstone': + return l10n.notificationRuleTombstoneDescription; + case '.m.rule.reaction': + return l10n.notificationRuleReactionDescription; + case '.m.rule.room_server_acl': + return l10n.notificationRuleRoomServerAclDescription; + case '.m.rule.suppress_edits': + return l10n.notificationRuleSuppressEditsDescription; + case '.m.rule.call': + return l10n.notificationRuleCallDescription; + case '.m.rule.encrypted_room_one_to_one': + return l10n.notificationRuleEncryptedRoomOneToOneDescription; + case '.m.rule.room_one_to_one': + return l10n.notificationRuleRoomOneToOneDescription; + case '.m.rule.message': + return l10n.notificationRuleMessageDescription; + case '.m.rule.encrypted': + return l10n.notificationRuleEncryptedDescription; + case '.m.rule.room.server_acl': + return l10n.notificationRuleServerAclDescription; + case '.im.vector.jitsi': + return l10n.notificationRuleJitsiDescription; + default: + return l10n.unknownPushRule(ruleId); + } + } +} + +extension PushRuleKindLocal on PushRuleKind { + String localized(L10n l10n) { + switch (this) { + case PushRuleKind.content: + return l10n.contentNotificationSettings; + case PushRuleKind.override: + return l10n.generalNotificationSettings; + case PushRuleKind.room: + return l10n.roomNotificationSettings; + case PushRuleKind.sender: + return l10n.userSpecificNotificationSettings; + case PushRuleKind.underride: + return l10n.otherNotificationSettings; + } + } +} + +extension on String { + String capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } +} diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 772cdef5a..476078f25 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -10,50 +9,6 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_notifications_view.dart'; -class NotificationSettingsItem { - final PushRuleKind type; - final String key; - final String Function(BuildContext) title; - const NotificationSettingsItem(this.type, this.key, this.title); - static List items = [ - NotificationSettingsItem( - PushRuleKind.underride, - '.m.rule.message', - (c) => L10n.of(c).allRooms, - ), - NotificationSettingsItem( - PushRuleKind.underride, - '.m.rule.room_one_to_one', - (c) => L10n.of(c).directChats, - ), - NotificationSettingsItem( - PushRuleKind.override, - '.m.rule.contains_display_name', - (c) => L10n.of(c).containsDisplayName, - ), - NotificationSettingsItem( - PushRuleKind.content, - '.m.rule.contains_user_name', - (c) => L10n.of(c).containsUserName, - ), - NotificationSettingsItem( - PushRuleKind.override, - '.m.rule.invite_for_me', - (c) => L10n.of(c).inviteForMe, - ), - NotificationSettingsItem( - PushRuleKind.override, - '.m.rule.member_event', - (c) => L10n.of(c).memberChanges, - ), - NotificationSettingsItem( - PushRuleKind.override, - '.m.rule.suppress_notices', - (c) => L10n.of(c).botMessages, - ), - ]; -} - class SettingsNotifications extends StatefulWidget { const SettingsNotifications({super.key}); @@ -63,80 +18,8 @@ class SettingsNotifications extends StatefulWidget { } class SettingsNotificationsController extends State { - bool? getNotificationSetting(NotificationSettingsItem item) { - final pushRules = Matrix.of(context).client.globalPushRules; - if (pushRules == null) return null; - switch (item.type) { - case PushRuleKind.content: - return pushRules.content - ?.singleWhereOrNull((r) => r.ruleId == item.key) - ?.enabled; - case PushRuleKind.override: - return pushRules.override - ?.singleWhereOrNull((r) => r.ruleId == item.key) - ?.enabled; - case PushRuleKind.room: - return pushRules.room - ?.singleWhereOrNull((r) => r.ruleId == item.key) - ?.enabled; - case PushRuleKind.sender: - return pushRules.sender - ?.singleWhereOrNull((r) => r.ruleId == item.key) - ?.enabled; - case PushRuleKind.underride: - return pushRules.underride - ?.singleWhereOrNull((r) => r.ruleId == item.key) - ?.enabled; - } - } - bool isLoading = false; - void setNotificationSetting( - NotificationSettingsItem item, - bool enabled, - ) async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - setState(() { - isLoading = true; - }); - try { - await Matrix.of(context).client.setPushRuleEnabled( - item.type, - item.key, - enabled, - ); - } catch (e, s) { - Logs().w('Unable to change notification settings', e, s); - scaffoldMessenger - .showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); - } finally { - setState(() { - isLoading = false; - }); - } - } - - void onToggleMuteAllNotifications() async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - setState(() { - isLoading = true; - }); - try { - await Matrix.of(context).client.setMuteAllPushNotifications( - !Matrix.of(context).client.allPushNotificationsMuted, - ); - } catch (e, s) { - Logs().w('Unable to change notification settings', e, s); - scaffoldMessenger - .showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); - } finally { - setState(() { - isLoading = false; - }); - } - } - void onPusherTap(Pusher pusher) async { final delete = await showModalActionPopup( context: context, @@ -172,6 +55,43 @@ class SettingsNotificationsController extends State { Future?>? pusherFuture; + void togglePushRule(PushRuleKind kind, PushRule pushRule) async { + setState(() { + isLoading = true; + }); + try { + final updateFromSync = Matrix.of(context) + .client + .onSync + .stream + .where( + (syncUpdate) => + syncUpdate.accountData?.any( + (accountData) => accountData.type == 'm.push_rules', + ) ?? + false, + ) + .first; + await Matrix.of(context).client.setPushRuleEnabled( + kind, + pushRule.ruleId, + !pushRule.enabled, + ); + await updateFromSync; + } catch (e, s) { + Logs().w('Unable to toggle push rule', e, s); + if (!mounted) return; + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); + } finally { + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } + @override Widget build(BuildContext context) => SettingsNotificationsView(this); } diff --git a/lib/pages/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_notifications/settings_notifications_view.dart index ed934c539..a51ce7673 100644 --- a/lib/pages/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_notifications/settings_notifications_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/pages/settings_notifications/push_rule_extensions.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import '../../utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; @@ -15,6 +16,17 @@ class SettingsNotificationsView extends StatelessWidget { @override Widget build(BuildContext context) { + final pushRules = Matrix.of(context).client.globalPushRules; + final pushCategories = [ + if (pushRules?.override?.isNotEmpty ?? false) + (rules: pushRules?.override ?? [], kind: PushRuleKind.override), + if (pushRules?.content?.isNotEmpty ?? false) + (rules: pushRules?.content ?? [], kind: PushRuleKind.content), + if (pushRules?.sender?.isNotEmpty ?? false) + (rules: pushRules?.sender ?? [], kind: PushRuleKind.sender), + if (pushRules?.underride?.isNotEmpty ?? false) + (rules: pushRules?.underride ?? [], kind: PushRuleKind.underride), + ]; return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), @@ -33,39 +45,36 @@ class SettingsNotificationsView extends StatelessWidget { final theme = Theme.of(context); return Column( children: [ - SwitchListTile.adaptive( - value: !Matrix.of(context).client.allPushNotificationsMuted, - title: Text( - L10n.of(context).notificationsEnabledForThisAccount, - ), - onChanged: controller.isLoading - ? null - : (_) => controller.onToggleMuteAllNotifications(), - ), - Divider(color: theme.dividerColor), - ListTile( - title: Text( - L10n.of(context).notifyMeFor, - style: TextStyle( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.bold, + if (pushRules != null) + for (final category in pushCategories) ...[ + ListTile( + title: Text( + category.kind.localized(L10n.of(context)), + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), ), - ), - ), - for (final item in NotificationSettingsItem.items) - SwitchListTile.adaptive( - value: Matrix.of(context).client.allPushNotificationsMuted - ? false - : controller.getNotificationSetting(item) ?? true, - title: Text(item.title(context)), - onChanged: controller.isLoading - ? null - : Matrix.of(context).client.allPushNotificationsMuted + for (final rule in category.rules) + SwitchListTile.adaptive( + value: rule.enabled, + title: Text(rule.getPushRuleName(L10n.of(context))), + subtitle: + Text(rule.getPushRuleDescription(L10n.of(context))), + onChanged: controller.isLoading ? null - : (bool enabled) => controller - .setNotificationSetting(item, enabled), - ), - Divider(color: theme.dividerColor), + : Matrix.of(context) + .client + .allPushNotificationsMuted + ? null + : (_) => controller.togglePushRule( + category.kind, + rule, + ), + ), + Divider(color: theme.dividerColor), + ], ListTile( title: Text( L10n.of(context).devices,