Merge pull request from vector-im/feature/adm/email_notification_toggle

Add email notification toggle
This commit is contained in:
Benoit Marty 2021-09-24 20:42:44 +02:00 committed by GitHub
commit 045e4bbf76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 325 additions and 41 deletions

1
changelog.d/2243.feature Normal file
View file

@ -0,0 +1 @@
Adds email notification registration to Settings

View file

@ -26,7 +26,14 @@ data class Pusher(
val data: PusherData, val data: PusherData,
val state: PusherState val state: PusherState
) ) {
companion object {
const val KIND_EMAIL = "email"
const val KIND_HTTP = "http"
const val APP_ID_EMAIL = "m.email"
}
}
enum class PusherState { enum class PusherState {
UNREGISTERED, UNREGISTERED,

View file

@ -27,14 +27,12 @@ interface PushersService {
/** /**
* Add a new HTTP pusher. * Add a new HTTP pusher.
* Note that only `http` kind is supported by the SDK for now.
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
* *
* @param pushkey This is a unique identifier for this pusher. The value you should use for * @param pushkey This is a unique identifier for this pusher. The value you should use for
* this is the routing or destination address information for the notification, * this is the routing or destination address information for the notification,
* for example, the APNS token for APNS or the Registration ID for GCM. If your * for example, the APNS token for APNS or the Registration ID for GCM. If your
* notification client has no such concept, use any unique identifier. Max length, 512 chars. * notification client has no such concept, use any unique identifier. Max length, 512 chars.
* If the kind is "email", this is the email address to send notifications to.
* @param appId the application id * @param appId the application id
* This is a reverse-DNS style identifier for the application. It is recommended * This is a reverse-DNS style identifier for the application. It is recommended
* that this end with the platform, such that different platform versions get * that this end with the platform, such that different platform versions get
@ -64,6 +62,30 @@ interface PushersService {
append: Boolean, append: Boolean,
withEventIdOnly: Boolean): UUID withEventIdOnly: Boolean): UUID
/**
* Add a new Email pusher.
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
*
* @param email The email address to send notifications to.
* @param lang The preferred language for receiving notifications (e.g. "en" or "en-US").
* @param emailBranding The branding placeholder to include in the email communications.
* @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher.
* @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher.
* @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition
* to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
* with the same App ID and pushkey for different users. Typically We always want to append for
* email pushers since we don't want to stop other accounts notifying to the same email address.
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
* @throws [InvalidParameterException] if a parameter is not correct
*/
fun addEmailPusher(email: String,
lang: String,
emailBranding: String,
appDisplayName: String,
deviceDisplayName: String,
append: Boolean = true): UUID
/** /**
* Directly ask the push gateway to send a push to this device * Directly ask the push gateway to send a push to this device
* If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId.
@ -80,10 +102,23 @@ interface PushersService {
eventId: String) eventId: String)
/** /**
* Remove the http pusher * Remove a registered pusher
* @param pusher the pusher to remove, can be http or email
*/
suspend fun removePusher(pusher: Pusher)
/**
* Remove a Http pusher by its pushkey and appId
* @see addHttpPusher
*/ */
suspend fun removeHttpPusher(pushkey: String, appId: String) suspend fun removeHttpPusher(pushkey: String, appId: String)
/**
* Remove an Email pusher
* @see addEmailPusher
*/
suspend fun removeEmailPusher(email: String)
/** /**
* Get the current pushers, as a LiveData * Get the current pushers, as a LiveData
*/ */

View file

@ -43,7 +43,7 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
import org.matrix.android.sdk.internal.session.media.MediaModule import org.matrix.android.sdk.internal.session.media.MediaModule
import org.matrix.android.sdk.internal.session.openid.OpenIdModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule
import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.pushers.PushersModule
import org.matrix.android.sdk.internal.session.room.RoomModule import org.matrix.android.sdk.internal.session.room.RoomModule
import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker
@ -127,7 +127,7 @@ internal interface SessionComponent {
fun inject(worker: SyncWorker) fun inject(worker: SyncWorker)
fun inject(worker: AddHttpPusherWorker) fun inject(worker: AddPusherWorker)
fun inject(worker: SendVerificationMessageWorker) fun inject(worker: SendVerificationMessageWorker)

View file

@ -33,8 +33,8 @@ import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import javax.inject.Inject import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) internal class AddPusherWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<AddHttpPusherWorker.Params>(context, params, Params::class.java) { : SessionSafeCoroutineWorker<AddPusherWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(

View file

@ -66,27 +66,45 @@ internal class DefaultPushersService @Inject constructor(
deviceDisplayName: String, deviceDisplayName: String,
url: String, url: String,
append: Boolean, append: Boolean,
withEventIdOnly: Boolean) withEventIdOnly: Boolean
: UUID { ) = addPusher(
// Do some parameter checks. It's ok to throw Exception, to inform developer of the problem JsonPusher(
if (pushkey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") pushKey = pushkey,
if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") kind = Pusher.KIND_HTTP,
if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") appId = appId,
profileTag = profileTag,
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }),
append = append
)
)
val pusher = JsonPusher( override fun addEmailPusher(email: String,
pushKey = pushkey, lang: String,
kind = "http", emailBranding: String,
appId = appId, appDisplayName: String,
appDisplayName = appDisplayName, deviceDisplayName: String,
deviceDisplayName = deviceDisplayName, append: Boolean
profileTag = profileTag, ) = addPusher(
lang = lang, JsonPusher(
data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), pushKey = email,
append = append) kind = Pusher.KIND_EMAIL,
appId = Pusher.APP_ID_EMAIL,
profileTag = "",
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(brand = emailBranding),
append = append
)
)
val params = AddHttpPusherWorker.Params(sessionId, pusher) private fun addPusher(pusher: JsonPusher): UUID {
pusher.validateParameters()
val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddHttpPusherWorker>() val params = AddPusherWorker.Params(sessionId, pusher)
val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setInputData(WorkerParamsFactory.toData(params)) .setInputData(WorkerParamsFactory.toData(params))
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
@ -95,8 +113,27 @@ internal class DefaultPushersService @Inject constructor(
return request.id return request.id
} }
private fun JsonPusher.validateParameters() {
// Do some parameter checks. It's ok to throw Exception, to inform developer of the problem
if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars")
if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars")
data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") }
}
override suspend fun removePusher(pusher: Pusher) {
removePusher(pusher.pushKey, pusher.appId)
}
override suspend fun removeHttpPusher(pushkey: String, appId: String) { override suspend fun removeHttpPusher(pushkey: String, appId: String) {
val params = RemovePusherTask.Params(pushkey, appId) removePusher(pushkey, appId)
}
override suspend fun removeEmailPusher(email: String) {
removePusher(pushKey = email, Pusher.APP_ID_EMAIL)
}
private suspend fun removePusher(pushKey: String, pushAppId: String) {
val params = RemovePusherTask.Params(pushKey, pushAppId)
removePusherTask.execute(params) removePusherTask.execute(params)
} }

View file

@ -32,5 +32,8 @@ internal data class JsonPusherData(
* Currently the only format available is 'event_id_only'. * Currently the only format available is 'event_id_only'.
*/ */
@Json(name = "format") @Json(name = "format")
val format: String? = null val format: String? = null,
@Json(name = "brand")
val brand: String? = null
) )

View file

@ -39,13 +39,15 @@ import im.vector.app.features.themes.ThemeUtils
/** /**
* Set a text in the TextView, or set visibility to GONE if the text is null * Set a text in the TextView, or set visibility to GONE if the text is null
*/ */
fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) { fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true, vararg relatedViews: View = emptyArray()) {
if (newText == null if (newText == null
|| (newText.isBlank() && hideWhenBlank)) { || (newText.isBlank() && hideWhenBlank)) {
isVisible = false isVisible = false
relatedViews.forEach { it.isVisible = false }
} else { } else {
this.text = newText this.text = newText
isVisible = true isVisible = true
relatedViews.forEach { it.isVisible = true }
} }
} }

View file

@ -61,6 +61,23 @@ class PushersManager @Inject constructor(
) )
} }
fun registerEmailForPush(email: String) {
val currentSession = activeSessionHolder.getActiveSession()
val appName = appNameProvider.getAppName()
currentSession.addEmailPusher(
email = email,
lang = localeProvider.current().language,
emailBranding = appName,
appDisplayName = appName,
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
)
}
suspend fun unregisterEmailPusher(email: String) {
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
currentSession.removeEmailPusher(email)
}
suspend fun unregisterPusher(pushKey: String) { suspend fun unregisterPusher(pushKey: String) {
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id))

View file

@ -117,6 +117,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
// notifications // notifications
const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"
const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"
const val SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY = "SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY"
// public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY"; // public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY";
const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY" const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY"

View file

@ -29,6 +29,7 @@ import im.vector.app.databinding.ActivityVectorSettingsBinding
import im.vector.app.features.discovery.DiscoverySettingsFragment import im.vector.app.features.discovery.DiscoverySettingsFragment
import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment
import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -136,6 +137,10 @@ class VectorSettingsActivity : VectorBaseActivity<ActivityVectorSettingsBinding>
return keyToHighlight return keyToHighlight
} }
override fun navigateToEmailAndPhoneNumbers() {
navigateTo(ThreePidsSettingsFragment::class.java)
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
if (ignoreInvalidTokenError) { if (ignoreInvalidTokenError) {
Timber.w("Ignoring invalid token global error") Timber.w("Ignoring invalid token global error")

View file

@ -20,4 +20,6 @@ interface VectorSettingsFragmentInteractionListener {
fun requestHighlightPreferenceKeyOnResume(key: String?) fun requestHighlightPreferenceKeyOnResume(key: String?)
fun requestedKeyToHighlight(): String? fun requestedKeyToHighlight(): String?
fun navigateToEmailAndPhoneNumbers()
} }

View file

@ -23,7 +23,10 @@ import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import im.vector.app.R import im.vector.app.R
@ -43,10 +46,14 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsBaseFragment
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.pushers.Pusher
import javax.inject.Inject import javax.inject.Inject
// Referenced in vector_settings_preferences_root.xml // Referenced in vector_settings_preferences_root.xml
@ -116,11 +123,51 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
} }
} }
bindEmailNotifications()
refreshBackgroundSyncPrefs() refreshBackgroundSyncPrefs()
handleSystemPreference() handleSystemPreference()
} }
private fun bindEmailNotifications() {
val initialEmails = session.getEmailsWithPushInformation()
bindEmailNotificationCategory(initialEmails)
session.getEmailsWithPushInformationLive().observe(this) { emails ->
if (initialEmails != emails) {
bindEmailNotificationCategory(emails)
}
}
}
private fun bindEmailNotificationCategory(emails: List<Pair<ThreePid.Email, Boolean>>) {
findPreference<VectorPreferenceCategory>(VectorPreferences.SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY)?.let { category ->
category.removeAll()
if (emails.isEmpty()) {
val vectorPreference = VectorPreference(requireContext())
vectorPreference.title = resources.getString(R.string.settings_notification_emails_no_emails)
category.addPreference(vectorPreference)
vectorPreference.setOnPreferenceClickListener {
interactionListener?.navigateToEmailAndPhoneNumbers()
true
}
} else {
emails.forEach { (emailPid, isEnabled) ->
val pref = VectorSwitchPreference(requireContext())
pref.title = resources.getString(R.string.settings_notification_emails_enable_for_email, emailPid.email)
pref.isChecked = isEnabled
pref.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
if (isChecked) {
pushManager.registerEmailForPush(emailPid.email)
} else {
pushManager.unregisterEmailPusher(emailPid.email)
}
}
category.addPreference(pref)
}
}
}
}
private val batteryStartForActivityResult = registerStartForActivityResult { private val batteryStartForActivityResult = registerStartForActivityResult {
// Noop // Noop
} }
@ -343,3 +390,43 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
} }
} }
} }
private fun SwitchPreference.setTransactionalSwitchChangeListener(scope: CoroutineScope, transaction: suspend (Boolean) -> Unit) {
setOnPreferenceChangeListener { switchPreference, isChecked ->
require(switchPreference is SwitchPreference)
val originalState = switchPreference.isChecked
scope.launch {
try {
transaction(isChecked as Boolean)
} catch (failure: Throwable) {
switchPreference.isChecked = originalState
Toast.makeText(switchPreference.context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
}
}
true
}
}
/**
* Fetches the current users 3pid emails and pairs them with their enabled state.
* If no pusher is available for a given email we can infer that push is not registered for the email.
* @return a list of ThreePid emails paired with the email notification enabled state. true if email notifications are enabled, false if not.
* @see ThreePid.Email
*/
private fun Session.getEmailsWithPushInformation(): List<Pair<ThreePid.Email, Boolean>> {
val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL }
return getThreePids()
.filterIsInstance<ThreePid.Email>()
.map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } }
}
private fun Session.getEmailsWithPushInformationLive(): LiveData<List<Pair<ThreePid.Email, Boolean>>> {
return getThreePidsLive(refreshData = false)
.distinctUntilChanged()
.map { threePids ->
val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL }
threePids
.filterIsInstance<ThreePid.Email>()
.map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } }
}
}

View file

@ -26,6 +26,8 @@ class PushGateWayController @Inject constructor(
private val stringProvider: StringProvider private val stringProvider: StringProvider
) : TypedEpoxyController<PushGatewayViewState>() { ) : TypedEpoxyController<PushGatewayViewState>() {
var interactionListener: PushGatewayItemInteractions? = null
override fun buildModels(data: PushGatewayViewState?) { override fun buildModels(data: PushGatewayViewState?) {
val host = this val host = this
data?.pushGateways?.invoke()?.let { pushers -> data?.pushGateways?.invoke()?.let { pushers ->
@ -39,6 +41,9 @@ class PushGateWayController @Inject constructor(
pushGatewayItem { pushGatewayItem {
id("${it.pushKey}_${it.appId}") id("${it.pushKey}_${it.appId}")
pusher(it) pusher(it)
host.interactionListener?.let {
interactions(it)
}
} }
} }
} }

View file

@ -17,7 +17,9 @@
package im.vector.app.features.settings.push package im.vector.app.features.settings.push
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.pushers.Pusher
sealed class PushGatewayAction : VectorViewModelAction { sealed class PushGatewayAction : VectorViewModelAction {
object Refresh : PushGatewayAction() object Refresh : PushGatewayAction()
data class RemovePusher(val pusher: Pusher) : PushGatewayAction()
} }

View file

@ -16,12 +16,14 @@
package im.vector.app.features.settings.push package im.vector.app.features.settings.push
import android.view.View
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.extensions.setTextOrHide
import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushers.Pusher
@EpoxyModelClass(layout = R.layout.item_pushgateway) @EpoxyModelClass(layout = R.layout.item_pushgateway)
@ -30,33 +32,45 @@ abstract class PushGatewayItem : EpoxyModelWithHolder<PushGatewayItem.Holder>()
@EpoxyAttribute @EpoxyAttribute
lateinit var pusher: Pusher lateinit var pusher: Pusher
@EpoxyAttribute
lateinit var interactions: PushGatewayItemInteractions
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.kind.text = when (pusher.kind) { holder.kind.text = when (pusher.kind) {
// TODO Create const Pusher.KIND_HTTP -> "Http Pusher"
"http" -> "Http Pusher" Pusher.KIND_EMAIL -> "Email Pusher"
"mail" -> "Email Pusher" else -> pusher.kind
else -> pusher.kind
} }
holder.appId.text = pusher.appId holder.appId.text = pusher.appId
holder.pushKey.text = pusher.pushKey holder.pushKey.text = pusher.pushKey
holder.appName.text = pusher.appDisplayName holder.appName.text = pusher.appDisplayName
holder.url.text = pusher.data.url holder.url.setTextOrHide(pusher.data.url, hideWhenBlank = true, holder.urlTitle)
holder.format.text = pusher.data.format holder.format.setTextOrHide(pusher.data.format, hideWhenBlank = true, holder.formatTitle)
holder.deviceName.text = pusher.deviceDisplayName holder.deviceName.text = pusher.deviceDisplayName
holder.removeButton.setOnClickListener {
interactions.onRemovePushTapped(pusher)
}
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val kind by bind<TextView>(R.id.pushGatewayKind) val kind by bind<TextView>(R.id.pushGatewayKind)
val pushKey by bind<TextView>(R.id.pushGatewayKeyValue) val pushKey by bind<TextView>(R.id.pushGatewayKeyValue)
val deviceName by bind<TextView>(R.id.pushGatewayDeviceNameValue) val deviceName by bind<TextView>(R.id.pushGatewayDeviceNameValue)
val formatTitle by bind<View>(R.id.pushGatewayFormat)
val format by bind<TextView>(R.id.pushGatewayFormatValue) val format by bind<TextView>(R.id.pushGatewayFormatValue)
val urlTitle by bind<View>(R.id.pushGatewayURL)
val url by bind<TextView>(R.id.pushGatewayURLValue) val url by bind<TextView>(R.id.pushGatewayURLValue)
val appName by bind<TextView>(R.id.pushGatewayAppNameValue) val appName by bind<TextView>(R.id.pushGatewayAppNameValue)
val appId by bind<TextView>(R.id.pushGatewayAppIdValue) val appId by bind<TextView>(R.id.pushGatewayAppIdValue)
val removeButton by bind<View>(R.id.pushGatewayDeleteButton)
} }
} }
interface PushGatewayItemInteractions {
fun onRemovePushTapped(pusher: Pusher)
}
// //
// abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleItem.Holder>() { // abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleItem.Holder>() {

View file

@ -0,0 +1,23 @@
/*
* 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.features.settings.push
import im.vector.app.core.platform.VectorViewEvents
sealed class PushGatewayViewEvents : VectorViewEvents {
data class RemovePusherFailed(val cause: Throwable): PushGatewayViewEvents()
}

View file

@ -24,11 +24,14 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import org.matrix.android.sdk.api.session.pushers.Pusher
import javax.inject.Inject import javax.inject.Inject
@ -64,7 +67,21 @@ class PushGatewaysFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
epoxyController.interactionListener = object : PushGatewayItemInteractions {
override fun onRemovePushTapped(pusher: Pusher) = viewModel.handle(PushGatewayAction.RemovePusher(pusher))
}
views.genericRecyclerView.configureWith(epoxyController, dividerDrawable = R.drawable.divider_horizontal) views.genericRecyclerView.configureWith(epoxyController, dividerDrawable = R.drawable.divider_horizontal)
viewModel.observeViewEvents {
when (it) {
is PushGatewayViewEvents.RemovePusherFailed -> {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(it.cause))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}.exhaustive
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.push package im.vector.app.features.settings.push
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
@ -26,8 +27,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.rx.RxSession import org.matrix.android.sdk.rx.RxSession
@ -38,7 +39,7 @@ data class PushGatewayViewState(
class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState, class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState,
private val session: Session) private val session: Session)
: VectorViewModel<PushGatewayViewState, PushGatewayAction, EmptyViewEvents>(initialState) { : VectorViewModel<PushGatewayViewState, PushGatewayAction, PushGatewayViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -70,10 +71,21 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: PushGatewayAction) { override fun handle(action: PushGatewayAction) {
when (action) { when (action) {
is PushGatewayAction.Refresh -> handleRefresh() is PushGatewayAction.Refresh -> handleRefresh()
is PushGatewayAction.RemovePusher -> removePusher(action.pusher)
}.exhaustive }.exhaustive
} }
private fun removePusher(pusher: Pusher) {
viewModelScope.launch {
kotlin.runCatching {
session.removePusher(pusher)
}.onFailure {
_viewEvents.post(PushGatewayViewEvents.RemovePusherFailed(it))
}
}
}
private fun handleRefresh() { private fun handleRefresh() {
session.refreshPushers() session.refreshPushers()
} }

View file

@ -120,7 +120,6 @@
android:text="@string/push_gateway_item_format" android:text="@string/push_gateway_item_format"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/pushGatewayFormatValue" android:id="@+id/pushGatewayFormatValue"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
@ -130,5 +129,11 @@
android:textStyle="" android:textStyle=""
tools:text="event_id_only" /> tools:text="event_id_only" />
<Button
android:id="@+id/pushGatewayDeleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/remove" />
</LinearLayout> </LinearLayout>

View file

@ -1112,6 +1112,12 @@
<string name="settings_notification_advanced">Advanced Notification Settings</string> <string name="settings_notification_advanced">Advanced Notification Settings</string>
<string name="settings_notification_by_event">Notification importance by event</string> <string name="settings_notification_by_event">Notification importance by event</string>
<string name="settings_notification_emails_category">Email notification</string>
<string name="settings_notification_emails_no_emails">To receive email with notification, please associate an email to your Matrix account</string>
<!-- The variable is a single email address, eg Enable email notifications for example@matrix.org -->
<string name="settings_notification_emails_enable_for_email">Enable email notifications for %s</string>
<string name="settings_notification_default">Default Notifications</string> <string name="settings_notification_default">Default Notifications</string>
<string name="settings_notification_mentions_and_keywords">Mentions and Keywords</string> <string name="settings_notification_mentions_and_keywords">Mentions and Keywords</string>
<string name="settings_notification_other">Other</string> <string name="settings_notification_other">Other</string>

View file

@ -55,6 +55,10 @@
</im.vector.app.core.preference.VectorPreferenceCategory> </im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY"
android:title="@string/settings_notification_emails_category"/>
<im.vector.app.core.preference.VectorPreferenceCategory <im.vector.app.core.preference.VectorPreferenceCategory
android:persistent="false" android:persistent="false"
android:title="@string/settings_notification_configuration"> android:title="@string/settings_notification_configuration">
@ -114,7 +118,6 @@
android:key="SETTINGS_START_ON_BOOT_PREFERENCE_KEY" android:key="SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
android:title="@string/settings_start_on_boot" /> android:title="@string/settings_start_on_boot" />
</im.vector.app.core.preference.VectorPreferenceCategory> </im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_troubleshoot_title"> <im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_troubleshoot_title">