mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #3894 from vector-im/feature/dla/keyword_notification_settings
Feature/dla/keyword notification settings
This commit is contained in:
commit
5b2478a34f
23 changed files with 539 additions and 42 deletions
1
changelog.d/3650.feature
Normal file
1
changelog.d/3650.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Mention and Keyword Notification Settings: Turn on/off keyword notifications and edit keywords.
|
|
@ -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>
|
|
@ -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/colorOnPrimary" android:state_enabled="false" />
|
||||
<item android:color="?attr/colorOnPrimary" />
|
||||
</selector>
|
15
library/ui-styles/src/main/res/values/styles_keyword.xml
Normal file
15
library/ui-styles/src/main/res/values/styles_keyword.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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="closeIconVisible">true</item>
|
||||
<item name="android:textColor">@color/keyword_foreground_selector</item>
|
||||
<item name="android:clickable">true</item>
|
||||
<item name="android:checkable">false</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -134,6 +134,8 @@
|
|||
<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>
|
||||
<!-- Keywords -->
|
||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
|
||||
|
|
|
@ -137,6 +137,9 @@
|
|||
|
||||
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
|
||||
|
||||
<!-- Keywords -->
|
||||
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
|
||||
</style>
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<Set<String>> {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RemovePushRuleTask.Params, Unit> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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<String> = linkedSetOf()
|
||||
|
||||
var keywords: Set<String>
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<VectorPreferenceCategory>("SETTINGS_DEFAULT")!!
|
||||
category.isIconSpaceReserved = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<VectorPreferenceCategory>("SETTINGS_KEYWORDS_AND_MENTIONS")!!
|
||||
mentionCategory.isIconSpaceReserved = false
|
||||
|
||||
val yourKeywordsCategory = findPreference<VectorPreferenceCategory>("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<KeywordPreference>("SETTINGS_KEYWORD_EDIT")!!
|
||||
editKeywordPreference.isEnabled = enableKeywords
|
||||
|
||||
val keywordPreference = findPreference<VectorCheckboxPreference>("SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_KEYWORDS_PREFERENCE_KEY")!!
|
||||
keywordPreference.isIconSpaceReserved = false
|
||||
keywordPreference.isChecked = enableKeywords
|
||||
|
||||
val footerPreference = findPreference<VectorPreference>("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<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
|
||||
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,
|
||||
|
|
|
@ -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<VectorPreferenceCategory>("SETTINGS_OTHER")!!
|
||||
category.isIconSpaceReserved = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<VectorCheckboxPreference>(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()
|
||||
}
|
||||
}
|
||||
|
|
57
vector/src/main/res/layout/vector_preference_chip_group.xml
Normal file
57
vector/src/main/res/layout/vector_preference_chip_group.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?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"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<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="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<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_new_keyword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/addKeywordButton"
|
||||
style="@style/Widget.Vector.Button.Positive"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/action_add" />
|
||||
|
||||
<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"
|
||||
android:paddingTop="16dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1105,6 +1105,10 @@
|
|||
<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">Add new keyword</string>
|
||||
<string name="settings_notification_keyword_contains_dot">Keywords cannot start with \'.\'</string>
|
||||
<string name="settings_notification_keyword_contains_invalid_character">Keywords cannot contain \'%s\'</string>
|
||||
|
||||
<string name="settings_notification_privacy">Notification privacy</string>
|
||||
<string name="settings_notification_troubleshoot">Troubleshoot Notifications</string>
|
||||
|
@ -1223,18 +1227,21 @@
|
|||
<string name="settings_messages_in_group_chat">Msgs in group chats</string>
|
||||
<string name="settings_invited_to_room">When I’m invited to a room</string>
|
||||
<string name="settings_messages_sent_by_bot">Messages sent by bot</string>
|
||||
<string name="settings_messages_at_room">Messages containing @room</string>
|
||||
|
||||
<string name="settings_messages_containing_display_name">Messages containing my display name</string>
|
||||
<string name="settings_messages_containing_username">Messages containing my username</string>
|
||||
<string name="settings_messages_containing_display_name">My display name</string>
|
||||
<string name="settings_messages_containing_username">My username</string>
|
||||
<string name="settings_messages_direct_messages">Direct messages</string>
|
||||
<string name="settings_encrypted_direct_messages">Encrypted direct messages</string>
|
||||
<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_mentions_at_room">\@room</string>
|
||||
<string name="settings_messages_containing_keywords">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>
|
||||
<string name="settings_room_upgrades">Room upgrades</string>
|
||||
<string name="settings_mentions_and_keywords_encryption_notice">You won’t get notifications for mentions & keywords in encrypted rooms on mobile.</string>
|
||||
|
||||
<string name="settings_background_sync">Background synchronization</string>
|
||||
<string name="settings_background_fdroid_sync_mode">Background Sync Mode</string>
|
||||
|
|
6
vector/src/main/res/xml/vector_settings_keyword_view.xml
Normal file
6
vector/src/main/res/xml/vector_settings_keyword_view.xml
Normal 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"
|
||||
/>
|
|
@ -17,7 +17,27 @@
|
|||
<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" />
|
||||
android:title="@string/settings_mentions_at_room" />
|
||||
|
||||
<im.vector.app.core.preference.VectorCheckboxPreference
|
||||
android:key="SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_KEYWORDS_PREFERENCE_KEY"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_messages_containing_keywords" />
|
||||
</im.vector.app.core.preference.VectorPreferenceCategory>
|
||||
|
||||
<im.vector.app.core.preference.VectorPreferenceCategory
|
||||
android:key="SETTINGS_YOUR_KEYWORDS"
|
||||
android:title="@string/settings_notification_your_keywords">
|
||||
<im.vector.app.core.preference.KeywordPreference
|
||||
android:key="SETTINGS_KEYWORD_EDIT"
|
||||
android:persistent="false"
|
||||
/>
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:key="SETTINGS_KEYWORDS_FOOTER"
|
||||
android:focusable="false"
|
||||
android:persistent="false"
|
||||
android:summary="@string/settings_mentions_and_keywords_encryption_notice" />
|
||||
|
||||
</im.vector.app.core.preference.VectorPreferenceCategory>
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in a new issue