diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 712db462d5..7e36f91590 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2621,6 +2621,11 @@ "searchSuggestion": "For now, search by typing a person’s name or public server address", "loadingContacts": "Loading contacts...", "recentlyChats": "Recent chats", + "groupNameChange": "Group chat name change", + "groupAvatarChange": "Group chat avatar change", + "setMeAsAdmin": "Set me as admin", + "mentionsMe": "Mention me", + "groupChat": "Group chat", "search": "Search", "forwardTo": "Forward to...", "noConnection": "No connection", diff --git a/docs/adr/0005-push-rules-decision.md b/docs/adr/0005-push-rules-decision.md new file mode 100644 index 0000000000..61ab308a93 --- /dev/null +++ b/docs/adr/0005-push-rules-decision.md @@ -0,0 +1,25 @@ +# 5. Push rules decision +Date: 2023-09-11 + +## Status + +Accepted + +## Context + +In the Matrix protocol, push rules define how messages are filtered and displayed on devices. These rules are customizable through APIs, allowing us to enable, disable, and specify actions for each rule. Furthermore, there are various types of push rules (override, content, sender, underride, etc.), each serving different purposes and enabling the configuration of push notifications. For reference, see the [Matrix Push Rules Specification](https://spec.matrix.org/v1.8/client-server-api/#push-rules). However, push rules have exhibited several limitations. For instance, notifications for group chats may still be received by users even after they have disabled them ([example](https://photos.app.goo.gl/JoaysSunovsdPtAs8)). + +## Decision + +To address these limitations, we propose the introduction of optional custom fields within the content of events. These custom fields can be utilized in the future to create custom push rules. For example, the `setName()` API for rooms lacks the capability for clients to determine whether the room already exists. To mitigate this, we suggest adding a custom field, such as `'roomState': 'created'`, to the content of `setName()` APIs. Subsequently, this `roomState` custom field can be utilized as a condition within push rules. To maintain consistency and facilitate tracking, we recommend defining metadata custom fields exclusively and documenting them in a constants file for easy reference by other developers. + +## Consequences + +- **Advantages:** + - Simplifies the definition of push rules, enhancing clarity and organization. + - Provides flexibility to define customized push rules. + - Promotes extendability and maintainability. + - Compatible with various home servers. + +- **Disadvantages:** + - Push notifications may behave inconsistently across different client applications. In essence, push rules are optimized for Twake applications and may not offer consistent behavior in other environments. For instance, in the Element application, notifications may be displayed for actions such as message sending or group name changes, regardless of user preferences regarding name change notifications. diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 5f2d5ff5ef..988cdfbd17 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -17,33 +17,43 @@ class NotificationSettingsItem { static List items = [ NotificationSettingsItem( PushRuleKind.underride, - '.m.rule.room_one_to_one', + '.m.rule.encrypted_room_one_to_one', (c) => L10n.of(c)!.directChats, ), + NotificationSettingsItem( + PushRuleKind.underride, + 'm.rule.encrypted_group_chat', + (c) => L10n.of(c)!.groupChat, + ), NotificationSettingsItem( PushRuleKind.override, - '.m.rule.contains_display_name', - (c) => L10n.of(c)!.containsDisplayName, + 'm.rule.invite_for_me', + (c) => L10n.of(c)!.inviteForMe, ), NotificationSettingsItem( - PushRuleKind.content, - '.m.rule.contains_user_name', - (c) => L10n.of(c)!.containsUserName, + PushRuleKind.override, + '.m.rule.suppress_notices', + (c) => L10n.of(c)!.botMessages, ), NotificationSettingsItem( PushRuleKind.override, - '.m.rule.invite_for_me', - (c) => L10n.of(c)!.inviteForMe, + 'm.rule.change_group_name', + (c) => L10n.of(c)!.groupNameChange, ), NotificationSettingsItem( PushRuleKind.override, - '.m.rule.member_event', - (c) => L10n.of(c)!.memberChanges, + 'm.rule.change_avatar_group', + (c) => L10n.of(c)!.groupAvatarChange, ), NotificationSettingsItem( PushRuleKind.override, - '.m.rule.suppress_notices', - (c) => L10n.of(c)!.botMessages, + 'm.rule.set_me_as_admin', + (c) => L10n.of(c)!.setMeAsAdmin, + ), + NotificationSettingsItem( + PushRuleKind.override, + '.m.rule.is_user_mention', + (c) => L10n.of(c)!.mentionsMe, ), ]; } diff --git a/lib/utils/matrix_sdk_extensions/client_push_rules_extension.dart b/lib/utils/matrix_sdk_extensions/client_push_rules_extension.dart new file mode 100644 index 0000000000..8a8a9803eb --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/client_push_rules_extension.dart @@ -0,0 +1,34 @@ +import 'package:matrix/matrix.dart'; + +extension ClientPushRulesExtension on Client { + Future setupUserDefinedPushRule({ + String scope = 'global', + required String ruleId, + List? conditions, + PushRuleKind kind = PushRuleKind.override, + String? after, + String? before, + }) async { + if (!containsRule(ruleId)) { + await setPushRule( + scope, + kind, + ruleId, + [ + PushRuleAction.notify, + {"set_tweak": "sound", "value": "default"} + ], + conditions: conditions, + after: after, + before: before, + ); + } + } + + bool containsRule(String ruleId) { + final rule = devicePushRules?.override?.firstWhere((PushRule rule) { + return rule.ruleId == ruleId; + }); + return rule?.ruleId == ruleId; + } +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 054e60069e..942b19f039 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/domain/model/tom_server_information.dart'; import 'package:fluffychat/domain/repository/tom_configurations_repository.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/client_push_rules_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; @@ -448,6 +449,94 @@ class MatrixState extends State with WidgetsBindingObserver { createVoipPlugin(); } + Future _setUpUserDefinedPushRules(Client client) async { + await Future.wait([ + client.setPushRuleEnabled( + 'global', + PushRuleKind.override, + '.m.rule.invite_for_me', + false, + ), + client.setPushRuleEnabled( + 'global', + PushRuleKind.override, + '.m.rule.member_event', + false, + ), + client.setupUserDefinedPushRule( + ruleId: 'm.rule.invite_for_me', + conditions: [ + PushCondition( + kind: 'event_match', + key: 'type', + pattern: EventTypes.RoomMember, + ), + PushCondition( + kind: 'event_match', + key: 'content.membership', + pattern: 'invite', + ), + PushCondition( + kind: 'event_match', + key: 'state_key', + pattern: '${client.userID}', + ), + ], + ), + client.setupUserDefinedPushRule( + ruleId: 'm.rule.set_me_as_admin', + conditions: [ + PushCondition( + kind: 'event_match', + key: 'type', + pattern: EventTypes.RoomPowerLevels, + ), + ], + ), + client.setupUserDefinedPushRule( + kind: PushRuleKind.underride, + ruleId: 'm.rule.encrypted_group_chat', + conditions: [ + PushCondition( + kind: 'event_match', + key: 'type', + pattern: EventTypes.Encrypted, + ), + PushCondition( + kind: 'event_match', + key: 'content.is_direct', + pattern: 'false', + ), + ], + ), + ]); + + await Future.wait([ + client.setupUserDefinedPushRule( + ruleId: 'm.rule.change_group_name', + conditions: [ + PushCondition( + kind: 'event_match', + key: 'type', + pattern: EventTypes.RoomName, + ), + ], + after: 'm.rule.invite_for_me', + ), + client.setupUserDefinedPushRule( + ruleId: 'm.rule.change_avatar_group', + conditions: [ + PushCondition( + kind: 'event_match', + key: 'type', + pattern: EventTypes.RoomAvatar, + ), + ], + after: 'm.rule.invite_for_me', + ), + ]); + } + void createVoipPlugin() async { if (await store.getItemBool(SettingKeys.experimentalVoip) == false) { voipPlugin = null; @@ -500,7 +589,9 @@ class MatrixState extends State with WidgetsBindingObserver { } if (homeServer != null) { _setUpHomeServer(homeServer.baseUrl); + _setUpUserDefinedPushRules(client); } + _storeToMConfiguration(client, tomServer, identityServer); setUpAuthorization(client); } @@ -522,7 +613,7 @@ class MatrixState extends State with WidgetsBindingObserver { } } - void _setUpHomeServer(Uri homeServerUri) { + void _setUpHomeServer(Uri homeServerUri) async { final homeServerUrlInterceptor = getIt.get( instanceName: NetworkDI.homeServerUrlInterceptorName, );