add keyword checkbox preference and chip/edit text for modificying keywords

This commit is contained in:
David Langley 2021-08-03 09:52:36 +01:00
parent 886bd3cc8f
commit 8d7e3b6544
20 changed files with 406 additions and 45 deletions

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/color_primary_alpha25" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.25" android:color="?attr/colorOnSecondary" android:state_enabled="false" />
<item android:color="?attr/colorOnSecondary" />
</selector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="vctr_keyword_style" format="reference" />
<style name="Widget.Vector.Keyword" parent="Widget.MaterialComponents.Chip.Action">
<item name="android:textAppearance">@style/TextAppearance.Vector.Body</item>
<item name="chipBackgroundColor">@color/keyword_background_selector</item>
<item name="closeIconTint">@color/keyword_foreground_selector</item>
<item name="android:textColor">@color/keyword_foreground_selector</item>
</style>
</resources>

View File

@ -134,7 +134,7 @@
<item name="vctr_social_login_button_gitlab_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Dark</item>
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Dark</item>
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
</style>
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

View File

@ -136,7 +136,7 @@
<item name="vctr_social_login_button_gitlab_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Light</item>
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
</style>
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />

View File

@ -15,6 +15,7 @@
*/
package org.matrix.android.sdk.api.pushrules
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
@ -39,7 +40,7 @@ interface PushRuleService {
suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?)
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
suspend fun removePushRule(kind: RuleKind, ruleId: String)
fun addPushRuleListener(listener: PushRuleListener)
@ -56,4 +57,6 @@ interface PushRuleService {
fun onEventRedacted(redactedEventId: String)
fun batchFinish()
}
fun getKeywords(): LiveData<Set<String>>
}

View File

@ -35,6 +35,8 @@ object RuleIds {
// Default Content Rules
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
const val RULE_ID_KEYWORDS = "_keywords"
// Default Underride Rules
const val RULE_ID_CALL = ".m.rule.call"
const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one"

View File

@ -15,6 +15,8 @@
*/
package org.matrix.android.sdk.internal.session.notification
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.pushrules.PushRuleService
@ -26,6 +28,7 @@ import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper
import org.matrix.android.sdk.internal.database.model.PushRuleEntity
import org.matrix.android.sdk.internal.database.model.PushRulesEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
@ -117,8 +120,8 @@ internal class DefaultPushRuleService @Inject constructor(
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, ruleId, enable, actions))
}
override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule))
override suspend fun removePushRule(kind: RuleKind, ruleId: String) {
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, ruleId))
}
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
@ -211,4 +214,18 @@ internal class DefaultPushRuleService @Inject constructor(
}
}
}
override fun getKeywords(): LiveData<Set<String>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm ->
PushRulesEntity.where(realm, RuleScope.GLOBAL, RuleSetKey.CONTENT)
},
{ result ->
result.pushRules.map(PushRuleEntity::ruleId).filter { !it.startsWith(".") }
}
)
return Transformations.map(liveData) { results ->
results.firstOrNull().orEmpty().toSet()
}
}
}

View File

@ -25,7 +25,7 @@ import javax.inject.Inject
internal interface RemovePushRuleTask : Task<RemovePushRuleTask.Params, Unit> {
data class Params(
val kind: RuleKind,
val pushRule: PushRule
val ruleId: String
)
}
@ -36,7 +36,7 @@ internal class DefaultRemovePushRuleTask @Inject constructor(
override suspend fun execute(params: RemovePushRuleTask.Params) {
return executeRequest(globalErrorReceiver) {
pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId)
pushRulesApi.deleteRule(params.kind.value, params.ruleId)
}
}
}

View File

@ -45,7 +45,7 @@ internal class DefaultSetRoomNotificationStateTask @Inject constructor(@SessionD
PushRuleEntity.where(it, scope = RuleScope.GLOBAL, ruleId = params.roomId).findFirst()?.toRoomPushRule()
}
if (currentRoomPushRule != null) {
removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule))
removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule.ruleId))
}
val newRoomPushRule = params.roomNotificationState.toRoomPushRule(params.roomId)
if (newRoomPushRule != null) {

View File

@ -143,8 +143,8 @@ android {
resValue "bool", "useLoginV2", "false"
// NotificationSettingsV2 is disabled. To be released in conjunction with iOS/Web
resValue "bool", "useNotificationSettingsV1", "true"
resValue "bool", "useNotificationSettingsV2", "false"
resValue "bool", "useNotificationSettingsV1", "false"
resValue "bool", "useNotificationSettingsV2", "true"
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.preference
import android.content.Context
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.preference.PreferenceViewHolder
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import im.vector.app.R
import java.util.SortedSet
class KeywordPreference : VectorPreference {
interface Listener {
fun didAddKeyword(keyword: String)
fun didRemoveKeyword(keyword: String)
}
var keywords: Set<String>
get() {
return _keywords
}
set(value) {
val newLinkedSet:LinkedHashSet<String> = linkedSetOf()
newLinkedSet.addAll(value.sorted())
_keywords = newLinkedSet
notifyChanged()
}
var listener: Listener? = null
private var _keywords: LinkedHashSet<String> = linkedSetOf()
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
init {
layoutResource = R.layout.vector_preference_chip_group
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.itemView.setOnClickListener(null)
holder.itemView.setOnLongClickListener(null)
val chipEditText = holder.findViewById(R.id.chipEditText) as? EditText ?: return
val chipGroup = holder.findViewById(R.id.chipGroup) as? ChipGroup ?: return
chipEditText.text = null
chipGroup.removeAllViews()
keywords.forEach {
addChipToGroup(it, chipGroup)
}
chipEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId != EditorInfo.IME_ACTION_DONE) {
return@setOnEditorActionListener false
}
val keyword = chipEditText.text.toString().trim()
if (keyword.isEmpty()){
return@setOnEditorActionListener false
}
_keywords.add(keyword)
listener?.didAddKeyword(keyword)
onPreferenceChangeListener?.onPreferenceChange(this, _keywords)
notifyChanged()
chipEditText.text = null
return@setOnEditorActionListener true
}
}
private fun addChipToGroup(keyword: String, chipGroup: ChipGroup) {
val chip = Chip(context, null, R.attr.vctr_keyword_style)
chip.text = keyword
chip.isClickable = true
chip.isCheckable = false
chip.isCloseIconVisible = true
chip.clipBounds
chipGroup.addView(chip)
chip.setOnCloseIconClickListener {
_keywords.remove(keyword)
listener?.didRemoveKeyword(keyword)
onPreferenceChangeListener?.onPreferenceChange(this, _keywords)
notifyChanged()
}
}
}

View File

@ -86,6 +86,12 @@ fun getStandardAction(ruleId: String, index: NotificationIndex): StandardActions
NotificationIndex.SILENT -> StandardActions.Notify
NotificationIndex.NOISY -> StandardActions.Highlight
}
RuleIds.RULE_ID_KEYWORDS ->
when(index) {
NotificationIndex.OFF -> StandardActions.Disabled
NotificationIndex.SILENT -> StandardActions.Notify
NotificationIndex.NOISY -> StandardActions.HighlightDefaultSound
}
else -> null
}
}

View File

@ -16,8 +16,24 @@
package im.vector.app.features.settings.notifications
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import im.vector.app.R
import im.vector.app.core.preference.KeywordPreference
import im.vector.app.core.preference.VectorCheckboxPreference
import im.vector.app.core.utils.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
import org.matrix.android.sdk.api.pushrules.toJson
import org.matrix.android.sdk.rx.rx
class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
: VectorSettingsPushRuleNotificationPreferenceFragment() {
@ -26,6 +42,131 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
override val preferenceXmlRes = R.xml.vector_settings_notification_mentions_and_keywords
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
session.getKeywords().observe(viewLifecycleOwner, this::updateWithKeywords)
}
override fun bindPref() {
super.bindPref()
val keywordRules = session.getPushRules().content?.filter { !it.ruleId.startsWith(".") }.orEmpty()
// val keywords = keywordRules.map(PushRule::ruleId).toSortedSet()
val keywordPreference = findPreference<VectorCheckboxPreference>("SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_KEYWORDS_PREFERENCE_KEY")!!
val anyEnabledKeywords = keywordRules.any(PushRule::enabled)
keywordPreference.isChecked = anyEnabledKeywords
keywordPreference.isEnabled = keywordRules.isNotEmpty()
var currentChecked = anyEnabledKeywords
keywordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val keywords = session.getKeywords().value ?: return@OnPreferenceChangeListener false
val newChecked = newValue as Boolean
displayLoadingView()
session.getKeywords()
updateKeywordPushRules(keywords, newChecked){ result ->
hideLoadingView()
if (!isAdded) {
return@updateKeywordPushRules
}
result.onSuccess {
currentChecked = newChecked
keywordPreference.isChecked = newChecked
}
result.onFailure {
refreshDisplay()
activity?.toast(errorFormatter.toHumanReadable(it))
}
}
false
}
val editKeywordPreference = findPreference<KeywordPreference>("SETTINGS_KEYWORD_EDIT")!!
editKeywordPreference.keywords = session.getKeywords().value.orEmpty()
editKeywordPreference.listener = object: KeywordPreference.Listener {
override fun didAddKeyword(keyword: String) {
addKeyword(keyword, currentChecked)
scrollToPreference(editKeywordPreference)
}
override fun didRemoveKeyword(keyword: String) {
removeKeyword(keyword)
scrollToPreference(editKeywordPreference)
}
}
}
fun updateKeywordPushRules(keywords: Set<String>, checked: Boolean, completion: (Result<Unit>) -> Unit) {
val newIndex = if (checked) NotificationIndex.NOISY else NotificationIndex.OFF
val standardAction = getStandardAction(RuleIds.RULE_ID_KEYWORDS, newIndex) ?: return
val enabled = standardAction != StandardActions.Disabled
val newActions = standardAction.actions
lifecycleScope.launch {
val results = keywords.map {
runCatching {
withContext(Dispatchers.Default) {
session.updatePushRuleActions(RuleKind.CONTENT,
it,
enabled,
newActions)
}
}
}
val firstError = results.firstNotNullOfOrNull(Result<Unit>::exceptionOrNull)
if (firstError == null){
completion(Result.success(Unit))
} else {
completion(Result.failure(firstError))
}
}
}
fun updateWithKeywords (keywords: Set<String>) {
val editKeywordPreference = findPreference<KeywordPreference>("SETTINGS_KEYWORD_EDIT") ?: return
editKeywordPreference.keywords = keywords
}
fun addKeyword(keyword: String, checked: Boolean) {
val newIndex = if (checked) NotificationIndex.NOISY else NotificationIndex.OFF
val standardAction = getStandardAction(RuleIds.RULE_ID_KEYWORDS, newIndex) ?: return
val enabled = standardAction != StandardActions.Disabled
val newActions = standardAction.actions ?: return
val newRule = PushRule(actions = newActions.toJson(), pattern = keyword, enabled = enabled, ruleId = keyword)
displayLoadingView()
lifecycleScope.launch {
val result = runCatching {
session.addPushRule(RuleKind.CONTENT, newRule)
}
if (!isAdded) {
return@launch
}
hideLoadingView()
// Already added to UI, no-op on success
result.onFailure {
// Just display an error on failure, keywords will update when push rules refreshed
activity?.toast(errorFormatter.toHumanReadable(it))
}
}
}
fun removeKeyword(keyword: String) {
displayLoadingView()
lifecycleScope.launch {
val result = runCatching {
session.removePushRule(RuleKind.CONTENT, keyword)
}
if (!isAdded) {
return@launch
}
hideLoadingView()
// Already added to UI, no-op on success
result.onFailure {
// Just display an error on failure, keywords will update when push rules refreshed
activity?.toast(errorFormatter.toHumanReadable(it))
}
}
}
override val prefKeyToPushRuleId = mapOf(
"SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME,
"SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_USER_NAME,

View File

@ -24,6 +24,7 @@ import im.vector.app.features.settings.VectorSettingsBaseFragment
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleSetKey
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
@ -104,16 +105,24 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
val initialIndex = ruleAndKind.pushRule.notificationIndex
preference.isChecked = initialIndex != NotificationIndex.OFF
preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val newIndex = if (newValue as Boolean) NotificationIndex.NOISY else NotificationIndex.OFF
val standardAction = getStandardAction(ruleAndKind.pushRule.ruleId, newIndex) ?: return@OnPreferenceChangeListener false
updatePushRule(ruleAndKind.pushRule.ruleId, ruleAndKind.kind,newValue as Boolean, preference)
false
}
}
}
}
fun updatePushRule(ruleId: String, kind: RuleKind, checked: Boolean, preference: VectorCheckboxPreference) {
val newIndex = if (checked) NotificationIndex.NOISY else NotificationIndex.OFF
val standardAction = getStandardAction(ruleId, newIndex) ?: return
val enabled = standardAction != StandardActions.Disabled
val newActions = standardAction.actions
displayLoadingView()
lifecycleScope.launch {
val result = runCatching {
session.updatePushRuleActions(ruleAndKind.kind,
ruleAndKind.pushRule.ruleId,
session.updatePushRuleActions(kind,
ruleId,
enabled,
newActions)
}
@ -122,7 +131,7 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
}
hideLoadingView()
result.onSuccess {
preference.isChecked = newValue
preference.isChecked = checked
}
result.onFailure { failure ->
// Restore the previous value
@ -130,14 +139,9 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
activity?.toast(errorFormatter.toHumanReadable(failure))
}
}
false
}
}
}
}
private fun refreshDisplay() {
fun refreshDisplay() {
listView?.adapter?.notifyDataSetChanged()
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/chipTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:errorEnabled="false"
app:hintEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:placeholderText="@string/settings_notification_new_keyword">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/chipEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="text"
android:imeOptions="actionDone"
android:hint="@string/settings_notification_your_keywords" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:chipSpacing="12dp"
app:lineSpacing="2dp"/>
</LinearLayout>

View File

@ -1092,6 +1092,8 @@
<string name="settings_notification_other">Other</string>
<string name="settings_notification_notify_me_for">Notify me for</string>
<string name="settings_notification_your_keywords">Your keywords</string>
<string name="settings_notification_new_keyword">New keyword</string>
<string name="settings_notification_privacy">Notification privacy</string>
<string name="settings_notification_troubleshoot">Troubleshoot Notifications</string>
@ -1218,6 +1220,7 @@
<string name="settings_group_messages">Group messages</string>
<string name="settings_encrypted_group_messages">Encrypted group messages</string>
<string name="settings_messages_at_room">Messages containing @room</string>
<string name="settings_messages_containing_keywords">Messages containing keywords</string>
<string name="settings_room_invitations">Room invitations</string>
<string name="settings_call_invitations">Call invitations</string>
<string name="settings_messages_by_bot">Messages by bot</string>

View File

@ -0,0 +1,6 @@
<chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:chipBackgroundColor="@color/keyword_background_selector"
app:closeIconTint="@color/keyword_foreground_selector"
/>

View File

@ -6,22 +6,18 @@
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_messages_direct_messages" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_IN_E2E_ONE_ONE_CHAT_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_encrypted_direct_messages" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_group_messages" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_IN_E2E_GROUP_CHAT_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_encrypted_group_messages" />
</im.vector.app.core.preference.VectorPreferenceCategory>

View File

@ -6,18 +6,23 @@
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_messages_containing_display_name" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_messages_containing_username" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_AT_ROOM_PREFERENCE_KEY"
android:persistent="false"
android:title="@string/settings_messages_at_room" />
<im.vector.app.core.preference.VectorCheckboxPreference
android:key="SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_KEYWORDS_PREFERENCE_KEY"
android:title="@string/settings_messages_containing_keywords" />
<im.vector.app.core.preference.KeywordPreference
android:key="SETTINGS_KEYWORD_EDIT"
/>
</im.vector.app.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen>