diff --git a/changelog.d/3650.feature b/changelog.d/3650.feature
new file mode 100644
index 0000000000..4ae065701f
--- /dev/null
+++ b/changelog.d/3650.feature
@@ -0,0 +1 @@
+Mention and Keyword Notification Settings: Turn on/off keyword notifications and edit keywords.
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/color/keyword_background_selector.xml b/library/ui-styles/src/main/res/color/keyword_background_selector.xml
new file mode 100644
index 0000000000..3420cfeaba
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/keyword_background_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml b/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml
new file mode 100644
index 0000000000..339f240246
--- /dev/null
+++ b/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/library/ui-styles/src/main/res/values/styles_keyword.xml b/library/ui-styles/src/main/res/values/styles_keyword.xml
new file mode 100644
index 0000000000..76e8eb4fc7
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/styles_keyword.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index 0dbdc5ad4f..f83953a527 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -134,6 +134,8 @@
- @style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Dark
- @style/Widget.Vector.JumpToUnread.Dark
+
+ - @style/Widget.Vector.Keyword
- @color/vctr_voice_message_toast_background_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 17e0ff2938..cd5e17d607 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -137,6 +137,9 @@
- @style/Widget.Vector.JumpToUnread.Light
+
+ - @style/Widget.Vector.Keyword
+
- @color/vctr_voice_message_toast_background_light
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index 4534368679..1d0acf38fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -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?)
- 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>
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt
index 4c01588b03..5b14e97d5e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt
@@ -35,6 +35,11 @@ object RuleIds {
// Default Content Rules
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
+ // The keywords rule id is not a "real" id in that it does not exist server-side.
+ // It is used client-side as a placeholder for rendering the keyword push rule setting
+ // alongside the others. A similar approach and naming is used on Web and iOS.
+ 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"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 4e8abcf784..65974151c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -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,19 @@ internal class DefaultPushRuleService @Inject constructor(
}
}
}
+
+ override fun getKeywords(): LiveData> {
+ // Keywords are all content rules that don't start with '.'
+ 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()
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt
index 23d0515f41..bae893608b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt
@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.pushers
import org.matrix.android.sdk.api.pushrules.RuleKind
-import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
@@ -25,7 +24,7 @@ import javax.inject.Inject
internal interface RemovePushRuleTask : Task {
data class Params(
val kind: RuleKind,
- val pushRule: PushRule
+ val ruleId: String
)
}
@@ -36,7 +35,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)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt
index de049d7538..9cea1fe425 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt
@@ -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) {
diff --git a/vector/build.gradle b/vector/build.gradle
index e867971cea..63860458b2 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -143,7 +143,7 @@ android {
resValue "bool", "useLoginV2", "false"
// NotificationSettingsV2 is disabled. To be released in conjunction with iOS/Web
- def useNotificationSettingsV2 = false
+ def useNotificationSettingsV2 = true
buildConfigField "Boolean", "USE_NOTIFICATION_SETTINGS_V2", "${useNotificationSettingsV2}"
resValue "bool", "useNotificationSettingsV1", "${!useNotificationSettingsV2}"
resValue "bool", "useNotificationSettingsV2", "${useNotificationSettingsV2}"
diff --git a/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt b/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt
new file mode 100644
index 0000000000..b57bb27671
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.text.Editable
+import android.util.AttributeSet
+import android.view.inputmethod.EditorInfo
+import android.widget.Button
+import android.widget.EditText
+import androidx.core.view.children
+import androidx.preference.PreferenceViewHolder
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
+import com.google.android.material.textfield.TextInputLayout
+import im.vector.app.R
+import im.vector.app.core.epoxy.addTextChangedListenerOnce
+import im.vector.app.core.platform.SimpleTextWatcher
+
+class KeywordPreference : VectorPreference {
+
+ interface Listener {
+ fun onFocusDidChange(hasFocus: Boolean)
+ fun didAddKeyword(keyword: String)
+ fun didRemoveKeyword(keyword: String)
+ }
+
+ private var keywordsEnabled = true
+ private var isCurrentKeywordValid = true
+
+ private var _keywords: LinkedHashSet = linkedSetOf()
+
+ var keywords: Set
+ get() {
+ return _keywords
+ }
+ set(value) {
+ // Updates existing `LinkedHashSet` vs assign a new set.
+ // This preserves the order added while on the screen (avoids keywords jumping around).
+ _keywords.removeAll(_keywords.filter { !value.contains(it) })
+ _keywords.addAll(value.sorted())
+ notifyChanged()
+ }
+
+ var listener: Listener? = null
+
+ 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 setEnabled(enabled: Boolean) {
+ super.setEnabled(enabled)
+ keywordsEnabled = enabled
+ notifyChanged()
+ }
+
+ 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
+ val addKeywordButton = holder.findViewById(R.id.addKeywordButton) as? Button ?: return
+ val chipTextInputLayout = holder.findViewById(R.id.chipTextInputLayout) as? TextInputLayout ?: return
+
+ chipEditText.text = null
+ chipGroup.removeAllViews()
+
+ keywords.forEach {
+ addChipToGroup(it, chipGroup)
+ }
+
+ chipEditText.isEnabled = keywordsEnabled
+ chipGroup.isEnabled = keywordsEnabled
+ chipGroup.children.forEach { it.isEnabled = keywordsEnabled }
+
+ chipEditText.addTextChangedListenerOnce(onTextChangeListener(chipTextInputLayout, addKeywordButton))
+ chipEditText.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId != EditorInfo.IME_ACTION_DONE) {
+ return@setOnEditorActionListener false
+ }
+ return@setOnEditorActionListener addKeyword(chipEditText)
+ }
+ chipEditText.setOnFocusChangeListener { _, hasFocus ->
+ listener?.onFocusDidChange(hasFocus)
+ }
+
+ addKeywordButton.setOnClickListener {
+ addKeyword(chipEditText)
+ }
+ }
+
+ private fun addKeyword(chipEditText: EditText): Boolean {
+ val keyword = chipEditText.text.toString().trim()
+
+ if (!isCurrentKeywordValid || keyword.isEmpty()) {
+ return false
+ }
+
+ listener?.didAddKeyword(keyword)
+ onPreferenceChangeListener?.onPreferenceChange(this, _keywords)
+ notifyChanged()
+ chipEditText.text = null
+ return true
+ }
+
+ private fun onTextChangeListener(chipTextInputLayout: TextInputLayout, addKeywordButton: Button) = object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ val keyword = s.toString().trim()
+ val errorMessage = when {
+ keyword.startsWith(".") -> {
+ context.getString(R.string.settings_notification_keyword_contains_dot)
+ }
+ keyword.contains("\\") -> {
+ context.getString(R.string.settings_notification_keyword_contains_invalid_character, "\\")
+ }
+ keyword.contains("/") -> {
+ context.getString(R.string.settings_notification_keyword_contains_invalid_character, "/")
+ }
+ else -> null
+ }
+
+ chipTextInputLayout.isErrorEnabled = errorMessage != null
+ chipTextInputLayout.error = errorMessage
+ val keywordValid = errorMessage == null
+ addKeywordButton.isEnabled = keywordsEnabled && keywordValid
+ this@KeywordPreference.isCurrentKeywordValid = keywordValid
+ }
+ }
+
+ private fun addChipToGroup(keyword: String, chipGroup: ChipGroup) {
+ val chip = Chip(context, null, R.attr.vctr_keyword_style)
+ chip.text = keyword
+ chipGroup.addView(chip)
+
+ chip.setOnCloseIconClickListener {
+ if (!keywordsEnabled) {
+ return@setOnCloseIconClickListener
+ }
+ listener?.didRemoveKeyword(keyword)
+ onPreferenceChangeListener?.onPreferenceChange(this, _keywords)
+ notifyChanged()
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
index 069216aaae..efbd1cd1b4 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
@@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import androidx.preference.PreferenceFragmentCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.di.DaggerScreenComponent
import im.vector.app.core.di.HasScreenInjector
@@ -160,9 +161,21 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree
}
activity?.runOnUiThread {
if (errorMessage != null && errorMessage.isNotBlank()) {
- activity?.toast(errorMessage)
+ displayErrorDialog(errorMessage)
}
hideLoadingView()
}
}
+
+ protected fun displayErrorDialog(throwable: Throwable) {
+ displayErrorDialog(errorFormatter.toHumanReadable(throwable))
+ }
+
+ protected fun displayErrorDialog(errorMessage: String) {
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorMessage)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/PushRuleDefinitions.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/PushRuleDefinitions.kt
index dd9077508e..d101d86aa3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/PushRuleDefinitions.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/PushRuleDefinitions.kt
@@ -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
}
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt
index 7d6b74b093..3fc6293e6b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.settings.notifications
import im.vector.app.R
+import im.vector.app.core.preference.VectorPreferenceCategory
import org.matrix.android.sdk.api.pushrules.RuleIds
class VectorSettingsDefaultNotificationPreferenceFragment
@@ -32,4 +33,10 @@ class VectorSettingsDefaultNotificationPreferenceFragment
"SETTINGS_PUSH_RULE_MESSAGES_IN_E2E_ONE_ONE_CHAT_PREFERENCE_KEY" to RuleIds.RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM,
"SETTINGS_PUSH_RULE_MESSAGES_IN_E2E_GROUP_CHAT_PREFERENCE_KEY" to RuleIds.RULE_ID_ENCRYPTED
)
+
+ override fun bindPref() {
+ super.bindPref()
+ val category = findPreference("SETTINGS_DEFAULT")!!
+ category.isIconSpaceReserved = false
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt
index 37acc1d898..59ed727191 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt
@@ -16,8 +16,22 @@
package im.vector.app.features.settings.notifications
+import android.os.Bundle
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+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.preference.VectorPreference
+import im.vector.app.core.preference.VectorPreferenceCategory
+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.toJson
class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
: VectorSettingsPushRuleNotificationPreferenceFragment() {
@@ -26,6 +40,139 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
override val preferenceXmlRes = R.xml.vector_settings_notification_mentions_and_keywords
+ private var keywordsHasFocus = false
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ session.getKeywords().observe(viewLifecycleOwner, this::updateWithKeywords)
+ }
+
+ override fun bindPref() {
+ super.bindPref()
+ val mentionCategory = findPreference("SETTINGS_KEYWORDS_AND_MENTIONS")!!
+ mentionCategory.isIconSpaceReserved = false
+
+ val yourKeywordsCategory = findPreference("SETTINGS_YOUR_KEYWORDS")!!
+ yourKeywordsCategory.isIconSpaceReserved = false
+
+ val keywordRules = session.getPushRules().content?.filter { !it.ruleId.startsWith(".") }.orEmpty()
+ val enableKeywords = keywordRules.isEmpty() || keywordRules.any(PushRule::enabled)
+
+ val editKeywordPreference = findPreference("SETTINGS_KEYWORD_EDIT")!!
+ editKeywordPreference.isEnabled = enableKeywords
+
+ val keywordPreference = findPreference("SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_KEYWORDS_PREFERENCE_KEY")!!
+ keywordPreference.isIconSpaceReserved = false
+ keywordPreference.isChecked = enableKeywords
+
+ val footerPreference = findPreference("SETTINGS_KEYWORDS_FOOTER")!!
+ footerPreference.isIconSpaceReserved = false
+ keywordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ val keywords = editKeywordPreference.keywords
+ val newChecked = newValue as Boolean
+ displayLoadingView()
+ updateKeywordPushRules(keywords, newChecked) { result ->
+ hideLoadingView()
+ if (!isAdded) {
+ return@updateKeywordPushRules
+ }
+ result.onSuccess {
+ keywordPreference.isChecked = newChecked
+ editKeywordPreference.isEnabled = newChecked
+ }
+ result.onFailure { failure ->
+ refreshDisplay()
+ displayErrorDialog(failure)
+ }
+ }
+ false
+ }
+
+ editKeywordPreference.listener = object: KeywordPreference.Listener {
+ override fun onFocusDidChange(hasFocus: Boolean) {
+ keywordsHasFocus = true
+ }
+
+ override fun didAddKeyword(keyword: String) {
+ addKeyword(keyword)
+ }
+
+ override fun didRemoveKeyword(keyword: String) {
+ removeKeyword(keyword)
+ }
+ }
+ }
+
+ fun updateKeywordPushRules(keywords: Set, checked: Boolean, completion: (Result) -> 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::exceptionOrNull)
+ if (firstError == null) {
+ completion(Result.success(Unit))
+ } else {
+ completion(Result.failure(firstError))
+ }
+ }
+ }
+
+ fun updateWithKeywords(keywords: Set) {
+ val editKeywordPreference = findPreference("SETTINGS_KEYWORD_EDIT") ?: return
+ editKeywordPreference.keywords = keywords
+ if (keywordsHasFocus) {
+ scrollToPreference(editKeywordPreference)
+ }
+ }
+
+ fun addKeyword(keyword: String) {
+ val standardAction = getStandardAction(RuleIds.RULE_ID_KEYWORDS, NotificationIndex.NOISY) ?: 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)
+ }
+ hideLoadingView()
+ if (!isAdded) {
+ return@launch
+ }
+ // Already added to UI, no-op on success
+
+ result.onFailure(::displayErrorDialog)
+ }
+ }
+
+ fun removeKeyword(keyword: String) {
+ displayLoadingView()
+ lifecycleScope.launch {
+ val result = runCatching {
+ session.removePushRule(RuleKind.CONTENT, keyword)
+ }
+ hideLoadingView()
+ if (!isAdded) {
+ return@launch
+ }
+ // Already added to UI, no-op on success
+
+ result.onFailure(::displayErrorDialog)
+ }
+ }
+
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,
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt
index 42203fb613..2e083a7d65 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.settings.notifications
import im.vector.app.R
+import im.vector.app.core.preference.VectorPreferenceCategory
import org.matrix.android.sdk.api.pushrules.RuleIds
class VectorSettingsOtherNotificationPreferenceFragment
@@ -32,4 +33,10 @@ class VectorSettingsOtherNotificationPreferenceFragment
"SETTINGS_PUSH_RULE_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY" to RuleIds.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS,
"SETTINGS_PUSH_RULE_ROOMS_UPGRADED_KEY" to RuleIds.RULE_ID_TOMBSTONE
)
+
+ override fun bindPref() {
+ super.bindPref()
+ val category = findPreference("SETTINGS_OTHER")!!
+ category.isIconSpaceReserved = false
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt
index 6f28876e1d..dbf33f8fb3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt
@@ -19,9 +19,9 @@ package im.vector.app.features.settings.notifications
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import im.vector.app.core.preference.VectorCheckboxPreference
-import im.vector.app.core.utils.toast
import im.vector.app.features.settings.VectorSettingsBaseFragment
import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
abstract class VectorSettingsPushRuleNotificationPreferenceFragment
@@ -32,6 +32,7 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
override fun bindPref() {
for (preferenceKey in prefKeyToPushRuleId.keys) {
val preference = findPreference(preferenceKey)!!
+ preference.isIconSpaceReserved = false
val ruleAndKind: PushRuleAndKind? = session.getPushRules().findDefaultRule(prefKeyToPushRuleId[preferenceKey])
if (ruleAndKind == null) {
// The rule is not defined, hide the preference
@@ -41,40 +42,43 @@ 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
- val enabled = standardAction != StandardActions.Disabled
- val newActions = standardAction.actions
- displayLoadingView()
-
- lifecycleScope.launch {
- val result = runCatching {
- session.updatePushRuleActions(ruleAndKind.kind,
- ruleAndKind.pushRule.ruleId,
- enabled,
- newActions)
- }
- if (!isAdded) {
- return@launch
- }
- hideLoadingView()
- result.onSuccess {
- preference.isChecked = newValue
- }
- result.onFailure { failure ->
- // Restore the previous value
- refreshDisplay()
- activity?.toast(errorFormatter.toHumanReadable(failure))
- }
- }
-
+ updatePushRule(ruleAndKind.pushRule.ruleId, ruleAndKind.kind, newValue as Boolean, preference)
false
}
}
}
}
- private fun refreshDisplay() {
+ 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(kind,
+ ruleId,
+ enabled,
+ newActions)
+ }
+ hideLoadingView()
+ if (!isAdded) {
+ return@launch
+ }
+ result.onSuccess {
+ preference.isChecked = checked
+ }
+ result.onFailure { failure ->
+ // Restore the previous value
+ refreshDisplay()
+ displayErrorDialog(failure)
+ }
+ }
+ }
+
+ fun refreshDisplay() {
listView?.adapter?.notifyDataSetChanged()
}
}
diff --git a/vector/src/main/res/layout/vector_preference_chip_group.xml b/vector/src/main/res/layout/vector_preference_chip_group.xml
new file mode 100644
index 0000000000..42891f7277
--- /dev/null
+++ b/vector/src/main/res/layout/vector_preference_chip_group.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 405e12fae3..edc0ca58fc 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -1105,6 +1105,10 @@
Other
Notify me for
+ Your keywords
+ Add new keyword
+ Keywords cannot start with \'.\'
+ Keywords cannot contain \'%s\'
Notification privacy
Troubleshoot Notifications
@@ -1223,18 +1227,21 @@
Msgs in group chats
When I’m invited to a room
Messages sent by bot
+ Messages containing @room
- Messages containing my display name
- Messages containing my username
+ My display name
+ My username
Direct messages
Encrypted direct messages
Group messages
Encrypted group messages
- Messages containing @room
+ \@room
+ Keywords
Room invitations
Call invitations
Messages by bot
Room upgrades
+ You won’t get notifications for mentions & keywords in encrypted rooms on mobile.
Background synchronization
Background Sync Mode
diff --git a/vector/src/main/res/xml/vector_settings_keyword_view.xml b/vector/src/main/res/xml/vector_settings_keyword_view.xml
new file mode 100644
index 0000000000..ed6ed8e32a
--- /dev/null
+++ b/vector/src/main/res/xml/vector_settings_keyword_view.xml
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/vector/src/main/res/xml/vector_settings_notification_mentions_and_keywords.xml b/vector/src/main/res/xml/vector_settings_notification_mentions_and_keywords.xml
index be89c86cb1..2de189cf7f 100644
--- a/vector/src/main/res/xml/vector_settings_notification_mentions_and_keywords.xml
+++ b/vector/src/main/res/xml/vector_settings_notification_mentions_and_keywords.xml
@@ -17,7 +17,27 @@
+ android:title="@string/settings_mentions_at_room" />
+
+
+
+
+
+
+
+
\ No newline at end of file