Show error dialog rather than toast. Add validation to keyword field for slashes and dots.

This commit is contained in:
David Langley 2021-08-27 15:48:11 +01:00
parent 9d663413a9
commit 68eaefbc9c
5 changed files with 78 additions and 34 deletions

View file

@ -17,6 +17,7 @@
package im.vector.app.core.preference package im.vector.app.core.preference
import android.content.Context import android.content.Context
import android.text.Editable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.Button import android.widget.Button
@ -25,7 +26,10 @@ import androidx.core.view.children
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.addTextChangedListenerOnce
import im.vector.app.core.platform.SimpleTextWatcher
class KeywordPreference : VectorPreference { class KeywordPreference : VectorPreference {
@ -36,6 +40,7 @@ class KeywordPreference : VectorPreference {
} }
private var keywordsEnabled = true private var keywordsEnabled = true
private var isCurrentKeywordValid = true
private var _keywords: LinkedHashSet<String> = linkedSetOf() private var _keywords: LinkedHashSet<String> = linkedSetOf()
@ -78,6 +83,7 @@ class KeywordPreference : VectorPreference {
val chipEditText = holder.findViewById(R.id.chipEditText) as? EditText ?: return val chipEditText = holder.findViewById(R.id.chipEditText) as? EditText ?: return
val chipGroup = holder.findViewById(R.id.chipGroup) as? ChipGroup ?: return val chipGroup = holder.findViewById(R.id.chipGroup) as? ChipGroup ?: return
val addKeywordButton = holder.findViewById(R.id.addKeywordButton) as? Button ?: return val addKeywordButton = holder.findViewById(R.id.addKeywordButton) as? Button ?: return
val chipTextInputLayout = holder.findViewById(R.id.chipTextInputLayout) as? TextInputLayout ?: return
chipEditText.text = null chipEditText.text = null
chipGroup.removeAllViews() chipGroup.removeAllViews()
@ -90,30 +96,57 @@ class KeywordPreference : VectorPreference {
chipGroup.isEnabled = keywordsEnabled chipGroup.isEnabled = keywordsEnabled
chipGroup.children.forEach { it.isEnabled = keywordsEnabled } chipGroup.children.forEach { it.isEnabled = keywordsEnabled }
fun addKeyword(): Boolean { chipEditText.addTextChangedListenerOnce(onTextChangeListener(chipTextInputLayout, addKeywordButton))
val keyword = chipEditText.text.toString().trim()
if (keyword.isEmpty()) {
return false
}
listener?.didAddKeyword(keyword)
onPreferenceChangeListener?.onPreferenceChange(this, _keywords)
notifyChanged()
chipEditText.text = null
return true
}
chipEditText.setOnEditorActionListener { _, actionId, _ -> chipEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId != EditorInfo.IME_ACTION_DONE) { if (actionId != EditorInfo.IME_ACTION_DONE) {
return@setOnEditorActionListener false return@setOnEditorActionListener false
} }
return@setOnEditorActionListener addKeyword() return@setOnEditorActionListener addKeyword(chipEditText)
} }
chipEditText.setOnFocusChangeListener { _, hasFocus -> chipEditText.setOnFocusChangeListener { _, hasFocus ->
listener?.onFocusDidChange(hasFocus) listener?.onFocusDidChange(hasFocus)
} }
addKeywordButton.setOnClickListener { addKeywordButton.setOnClickListener {
addKeyword() 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
} }
} }

View file

@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.DaggerScreenComponent import im.vector.app.core.di.DaggerScreenComponent
import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.di.HasScreenInjector
@ -160,9 +161,22 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree
} }
activity?.runOnUiThread { activity?.runOnUiThread {
if (errorMessage != null && errorMessage.isNotBlank()) { if (errorMessage != null && errorMessage.isNotBlank()) {
activity?.toast(errorMessage) displayErrorDialog(errorMessage)
} }
hideLoadingView() 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()
}
} }

View file

@ -25,7 +25,6 @@ import im.vector.app.core.preference.KeywordPreference
import im.vector.app.core.preference.VectorCheckboxPreference import im.vector.app.core.preference.VectorCheckboxPreference
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.utils.toast
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -68,7 +67,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
val footerPreference = findPreference<VectorPreference>("SETTINGS_KEYWORDS_FOOTER")!! val footerPreference = findPreference<VectorPreference>("SETTINGS_KEYWORDS_FOOTER")!!
footerPreference.isIconSpaceReserved = false footerPreference.isIconSpaceReserved = false
val host = this
keywordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> keywordPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val keywords = editKeywordPreference.keywords val keywords = editKeywordPreference.keywords
val newChecked = newValue as Boolean val newChecked = newValue as Boolean
@ -82,17 +81,15 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
keywordPreference.isChecked = newChecked keywordPreference.isChecked = newChecked
editKeywordPreference.isEnabled = newChecked editKeywordPreference.isEnabled = newChecked
} }
result.onFailure { result.onFailure { failure ->
refreshDisplay() refreshDisplay()
activity?.toast(errorFormatter.toHumanReadable(it)) host.displayErrorDialog(failure)
} }
} }
false false
} }
val host = this
editKeywordPreference.listener = object: KeywordPreference.Listener { editKeywordPreference.listener = object: KeywordPreference.Listener {
override fun onFocusDidChange(hasFocus: Boolean) { override fun onFocusDidChange(hasFocus: Boolean) {
host.keywordsHasFocus = true host.keywordsHasFocus = true
} }
@ -147,37 +144,35 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment
val newActions = standardAction.actions ?: return val newActions = standardAction.actions ?: return
val newRule = PushRule(actions = newActions.toJson(), pattern = keyword, enabled = enabled, ruleId = keyword) val newRule = PushRule(actions = newActions.toJson(), pattern = keyword, enabled = enabled, ruleId = keyword)
displayLoadingView() displayLoadingView()
val host = this
lifecycleScope.launch { lifecycleScope.launch {
val result = runCatching { val result = runCatching {
session.addPushRule(RuleKind.CONTENT, newRule) session.addPushRule(RuleKind.CONTENT, newRule)
} }
hideLoadingView()
if (!isAdded) { if (!isAdded) {
return@launch return@launch
} }
hideLoadingView()
// Already added to UI, no-op on success // Already added to UI, no-op on success
result.onFailure {
// Just display an error on failure, keywords have not been added to the UI result.onFailure(host::displayErrorDialog)
activity?.toast(errorFormatter.toHumanReadable(it))
}
} }
} }
fun removeKeyword(keyword: String) { fun removeKeyword(keyword: String) {
displayLoadingView() displayLoadingView()
val host = this
lifecycleScope.launch { lifecycleScope.launch {
val result = runCatching { val result = runCatching {
session.removePushRule(RuleKind.CONTENT, keyword) session.removePushRule(RuleKind.CONTENT, keyword)
} }
hideLoadingView()
if (!isAdded) { if (!isAdded) {
return@launch return@launch
} }
hideLoadingView()
// Already added to UI, no-op on success // Already added to UI, no-op on success
result.onFailure {
// Just display an error on failure, keywords have not been added to the UI result.onFailure(host::displayErrorDialog)
activity?.toast(errorFormatter.toHumanReadable(it))
}
} }
} }

View file

@ -19,7 +19,6 @@ package im.vector.app.features.settings.notifications
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import im.vector.app.core.preference.VectorCheckboxPreference import im.vector.app.core.preference.VectorCheckboxPreference
import im.vector.app.core.utils.toast
import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsBaseFragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
@ -57,6 +56,7 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
val newActions = standardAction.actions val newActions = standardAction.actions
displayLoadingView() displayLoadingView()
val host = this
lifecycleScope.launch { lifecycleScope.launch {
val result = runCatching { val result = runCatching {
session.updatePushRuleActions(kind, session.updatePushRuleActions(kind,
@ -64,17 +64,17 @@ abstract class VectorSettingsPushRuleNotificationPreferenceFragment
enabled, enabled,
newActions) newActions)
} }
hideLoadingView()
if (!isAdded) { if (!isAdded) {
return@launch return@launch
} }
hideLoadingView()
result.onSuccess { result.onSuccess {
preference.isChecked = checked preference.isChecked = checked
} }
result.onFailure { failure -> result.onFailure { failure ->
// Restore the previous value // Restore the previous value
refreshDisplay() refreshDisplay()
activity?.toast(errorFormatter.toHumanReadable(failure)) host.displayErrorDialog(failure)
} }
} }
} }

View file

@ -1097,6 +1097,8 @@
<string name="settings_notification_notify_me_for">Notify me for</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_your_keywords">Your keywords</string>
<string name="settings_notification_new_keyword">Add new keyword</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_privacy">Notification privacy</string>
<string name="settings_notification_troubleshoot">Troubleshoot Notifications</string> <string name="settings_notification_troubleshoot">Troubleshoot Notifications</string>