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 @@ + + + + + + + + + +