diff --git a/changelog.d/2243.feature b/changelog.d/2243.feature new file mode 100644 index 0000000000..428c5240d0 --- /dev/null +++ b/changelog.d/2243.feature @@ -0,0 +1 @@ +Adds email notification registration to Settings \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt index eed75c9daf..b85ab32b21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt @@ -26,7 +26,14 @@ data class Pusher( val data: PusherData, 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 { UNREGISTERED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index a5ec100f64..2cd17952c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -27,14 +27,12 @@ interface PushersService { /** * 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 * * @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, * 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. - * If the kind is "email", this is the email address to send notifications to. * @param appId the application id * 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 @@ -64,6 +62,30 @@ interface PushersService { append: Boolean, 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 status = workManager.getWorkInfoByIdLiveData()) + * @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 * 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) /** - * 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) + /** + * Remove an Email pusher + * @see addEmailPusher + */ + suspend fun removeEmailPusher(email: String) + /** * Get the current pushers, as a LiveData */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 9a936b73c2..2003a66c94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -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.openid.OpenIdModule 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.room.RoomModule import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker @@ -127,7 +127,7 @@ internal interface SessionComponent { fun inject(worker: SyncWorker) - fun inject(worker: AddHttpPusherWorker) + fun inject(worker: AddPusherWorker) fun inject(worker: SendVerificationMessageWorker) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt index c9d7ad2193..079fd1d3e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt @@ -33,8 +33,8 @@ import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import javax.inject.Inject -internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { +internal class AddPusherWorker(context: Context, params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index a772cf5ebb..9a50abfe35 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -66,27 +66,45 @@ internal class DefaultPushersService @Inject constructor( deviceDisplayName: String, url: String, append: Boolean, - withEventIdOnly: Boolean) - : UUID { - // 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") - if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") + withEventIdOnly: Boolean + ) = addPusher( + JsonPusher( + pushKey = pushkey, + kind = Pusher.KIND_HTTP, + appId = appId, + profileTag = profileTag, + lang = lang, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), + append = append + ) + ) - val pusher = JsonPusher( - pushKey = pushkey, - kind = "http", - appId = appId, - appDisplayName = appDisplayName, - deviceDisplayName = deviceDisplayName, - profileTag = profileTag, - lang = lang, - data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), - append = append) + override fun addEmailPusher(email: String, + lang: String, + emailBranding: String, + appDisplayName: String, + deviceDisplayName: String, + append: Boolean + ) = addPusher( + JsonPusher( + pushKey = email, + 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) - - val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() + private fun addPusher(pusher: JsonPusher): UUID { + pusher.validateParameters() + val params = AddPusherWorker.Params(sessionId, pusher) + val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) @@ -95,8 +113,27 @@ internal class DefaultPushersService @Inject constructor( 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) { - 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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt index c8d4d77fb1..42a8fa6ff3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt @@ -32,5 +32,8 @@ internal data class JsonPusherData( * Currently the only format available is 'event_id_only'. */ @Json(name = "format") - val format: String? = null + val format: String? = null, + + @Json(name = "brand") + val brand: String? = null ) diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index 1c424f7071..0872edeafd 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -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 */ -fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) { +fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true, vararg relatedViews: View = emptyArray()) { if (newText == null || (newText.isBlank() && hideWhenBlank)) { isVisible = false + relatedViews.forEach { it.isVisible = false } } else { this.text = newText isVisible = true + relatedViews.forEach { it.isVisible = true } } } diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 5896122393..a27765bf4f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -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) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 0b7b495f48..4341d381b6 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -117,6 +117,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { // notifications 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_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"; const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index 4546313198..caf42e7cf9 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -29,6 +29,7 @@ import im.vector.app.databinding.ActivityVectorSettingsBinding import im.vector.app.features.discovery.DiscoverySettingsFragment import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment 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.session.Session @@ -136,6 +137,10 @@ class VectorSettingsActivity : VectorBaseActivity return keyToHighlight } + override fun navigateToEmailAndPhoneNumbers() { + navigateTo(ThreePidsSettingsFragment::class.java) + } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { if (ignoreInvalidTokenError) { Timber.w("Ignoring invalid token global error") diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt index b815ce653d..ddfcc93287 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt @@ -20,4 +20,6 @@ interface VectorSettingsFragmentInteractionListener { fun requestHighlightPreferenceKeyOnResume(key: String?) fun requestedKeyToHighlight(): String? + + fun navigateToEmailAndPhoneNumbers() } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 40f575c853..098d1b2caa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -23,7 +23,10 @@ import android.media.RingtoneManager import android.net.Uri import android.os.Parcelable import android.widget.Toast +import androidx.lifecycle.LiveData +import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference 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.VectorSettingsFragmentInteractionListener import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds 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 // Referenced in vector_settings_preferences_root.xml @@ -116,11 +123,51 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + bindEmailNotifications() refreshBackgroundSyncPrefs() 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>) { + findPreference(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 { // 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> { + val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL } + return getThreePids() + .filterIsInstance() + .map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } } +} + +private fun Session.getEmailsWithPushInformationLive(): LiveData>> { + return getThreePidsLive(refreshData = false) + .distinctUntilChanged() + .map { threePids -> + val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL } + threePids + .filterIsInstance() + .map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt index 679f406832..6cb19b13c5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt @@ -26,6 +26,8 @@ class PushGateWayController @Inject constructor( private val stringProvider: StringProvider ) : TypedEpoxyController() { + var interactionListener: PushGatewayItemInteractions? = null + override fun buildModels(data: PushGatewayViewState?) { val host = this data?.pushGateways?.invoke()?.let { pushers -> @@ -39,6 +41,9 @@ class PushGateWayController @Inject constructor( pushGatewayItem { id("${it.pushKey}_${it.appId}") pusher(it) + host.interactionListener?.let { + interactions(it) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt index 566a068a7d..034b0b5ac7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt @@ -17,7 +17,9 @@ package im.vector.app.features.settings.push import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.pushers.Pusher sealed class PushGatewayAction : VectorViewModelAction { object Refresh : PushGatewayAction() + data class RemovePusher(val pusher: Pusher) : PushGatewayAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt index dc66e1983b..04aa2747d7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt @@ -16,12 +16,14 @@ package im.vector.app.features.settings.push +import android.view.View import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.extensions.setTextOrHide import org.matrix.android.sdk.api.session.pushers.Pusher @EpoxyModelClass(layout = R.layout.item_pushgateway) @@ -30,33 +32,45 @@ abstract class PushGatewayItem : EpoxyModelWithHolder() @EpoxyAttribute lateinit var pusher: Pusher + @EpoxyAttribute + lateinit var interactions: PushGatewayItemInteractions + override fun bind(holder: Holder) { super.bind(holder) holder.kind.text = when (pusher.kind) { - // TODO Create const - "http" -> "Http Pusher" - "mail" -> "Email Pusher" - else -> pusher.kind + Pusher.KIND_HTTP -> "Http Pusher" + Pusher.KIND_EMAIL -> "Email Pusher" + else -> pusher.kind } holder.appId.text = pusher.appId holder.pushKey.text = pusher.pushKey holder.appName.text = pusher.appDisplayName - holder.url.text = pusher.data.url - holder.format.text = pusher.data.format + holder.url.setTextOrHide(pusher.data.url, hideWhenBlank = true, holder.urlTitle) + holder.format.setTextOrHide(pusher.data.format, hideWhenBlank = true, holder.formatTitle) holder.deviceName.text = pusher.deviceDisplayName + holder.removeButton.setOnClickListener { + interactions.onRemovePushTapped(pusher) + } } class Holder : VectorEpoxyHolder() { val kind by bind(R.id.pushGatewayKind) val pushKey by bind(R.id.pushGatewayKeyValue) val deviceName by bind(R.id.pushGatewayDeviceNameValue) + val formatTitle by bind(R.id.pushGatewayFormat) val format by bind(R.id.pushGatewayFormatValue) + val urlTitle by bind(R.id.pushGatewayURL) val url by bind(R.id.pushGatewayURLValue) val appName by bind(R.id.pushGatewayAppNameValue) val appId by bind(R.id.pushGatewayAppIdValue) + val removeButton by bind(R.id.pushGatewayDeleteButton) } } +interface PushGatewayItemInteractions { + fun onRemovePushTapped(pusher: Pusher) +} + // // abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder() { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt new file mode 100644 index 0000000000..8b2a833b5c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt index be2457397d..cd1899741f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt @@ -24,11 +24,14 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cleanup 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.databinding.FragmentGenericRecyclerBinding +import org.matrix.android.sdk.api.session.pushers.Pusher import javax.inject.Inject @@ -64,7 +67,21 @@ class PushGatewaysFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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) + 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() { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt index 7981d71ce1..9a47fa2a15 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.push +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState @@ -26,8 +27,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.EmptyViewEvents 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.pushers.Pusher import org.matrix.android.sdk.rx.RxSession @@ -38,7 +39,7 @@ data class PushGatewayViewState( class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedFactory interface Factory { @@ -70,10 +71,21 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: PushGatewayAction) { when (action) { - is PushGatewayAction.Refresh -> handleRefresh() + is PushGatewayAction.Refresh -> handleRefresh() + is PushGatewayAction.RemovePusher -> removePusher(action.pusher) }.exhaustive } + private fun removePusher(pusher: Pusher) { + viewModelScope.launch { + kotlin.runCatching { + session.removePusher(pusher) + }.onFailure { + _viewEvents.post(PushGatewayViewEvents.RemovePusherFailed(it)) + } + } + } + private fun handleRefresh() { session.refreshPushers() } diff --git a/vector/src/main/res/layout/item_pushgateway.xml b/vector/src/main/res/layout/item_pushgateway.xml index 28bcef1ee2..d3b1f3ed76 100644 --- a/vector/src/main/res/layout/item_pushgateway.xml +++ b/vector/src/main/res/layout/item_pushgateway.xml @@ -120,7 +120,6 @@ android:text="@string/push_gateway_item_format" android:textStyle="bold" /> - +