Add ability to not show the pusher registration again.

This commit is contained in:
Benoit Marty 2024-06-17 11:15:47 +02:00
parent deb0e0aaac
commit 366d6c017d
13 changed files with 86 additions and 6 deletions

View file

@ -17,5 +17,5 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
sealed interface LoggedInEvents { sealed interface LoggedInEvents {
data object CloseErrorDialog : LoggedInEvents data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
} }

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
@ -41,6 +42,7 @@ import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -56,9 +58,13 @@ class LoggedInPresenter @Inject constructor(
) : Presenter<LoggedInState> { ) : Presenter<LoggedInState> {
@Composable @Composable
override fun present(): LoggedInState { override fun present(): LoggedInState {
val coroutineScope = rememberCoroutineScope()
val isVerified by remember { val isVerified by remember {
sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified } sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified }
}.collectAsState(initial = false) }.collectAsState(initial = false)
val ignoreRegistrationError by remember {
pushService.ignoreRegistrationError(matrixClient.sessionId)
}.collectAsState(initial = false)
val pusherRegistrationState = remember<MutableState<AsyncData<Unit>>> { mutableStateOf(AsyncData.Uninitialized) } val pusherRegistrationState = remember<MutableState<AsyncData<Unit>>> { mutableStateOf(AsyncData.Uninitialized) }
if (isVerified) { if (isVerified) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -84,13 +90,21 @@ class LoggedInPresenter @Inject constructor(
fun handleEvent(event: LoggedInEvents) { fun handleEvent(event: LoggedInEvents) {
when (event) { when (event) {
LoggedInEvents.CloseErrorDialog -> pusherRegistrationState.value = AsyncData.Uninitialized is LoggedInEvents.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) {
coroutineScope.launch {
pushService.setIgnoreRegistrationError(matrixClient.sessionId, true)
}
}
}
} }
} }
return LoggedInState( return LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value, pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
eventSink = ::handleEvent eventSink = ::handleEvent
) )
} }

View file

@ -21,5 +21,6 @@ import io.element.android.libraries.architecture.AsyncData
data class LoggedInState( data class LoggedInState(
val showSyncSpinner: Boolean, val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>, val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val eventSink: (LoggedInEvents) -> Unit, val eventSink: (LoggedInEvents) -> Unit,
) )

View file

@ -34,5 +34,6 @@ fun aLoggedInState(
) = LoggedInState( ) = LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState, pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false,
eventSink = {}, eventSink = {},
) )

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.exception.isNetworkError import io.element.android.libraries.matrix.api.exception.isNetworkError
@ -52,11 +52,12 @@ fun LoggedInView(
is AsyncData.Success -> Unit is AsyncData.Success -> Unit
is AsyncData.Failure -> { is AsyncData.Failure -> {
state.pusherRegistrationState.errorOrNull() state.pusherRegistrationState.errorOrNull()
?.takeIf { !state.ignoreRegistrationError }
?.getReason() ?.getReason()
?.let { reason -> ?.let { reason ->
ErrorDialog( ErrorDialogWithDoNotShowAgain(
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason), content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
onDismiss = { state.eventSink(LoggedInEvents.CloseErrorDialog) }, onDismiss = { state.eventSink(LoggedInEvents.CloseErrorDialog(it)) },
) )
} }
} }

View file

@ -64,6 +64,7 @@ class LoggedInPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse() assertThat(initialState.showSyncSpinner).isFalse()
assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue() assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(initialState.ignoreRegistrationError).isFalse()
skipItems(1) skipItems(1)
} }
} }
@ -356,6 +357,12 @@ class LoggedInPresenterTest {
.isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java) .isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
lambda.assertions() lambda.assertions()
.isNeverCalled() .isNeverCalled()
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
skipItems(1)
val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(lastState.ignoreRegistrationError).isTrue()
} }
} }
@ -390,7 +397,7 @@ class LoggedInPresenterTest {
lambda.assertions() lambda.assertions()
.isNeverCalled() .isNeverCalled()
// Reset the error // Reset the error
finalState.eventSink(LoggedInEvents.CloseErrorDialog) finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
val lastState = awaitItem() val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue() assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
} }

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +48,7 @@ class DefaultClearCacheUseCase @Inject constructor(
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
private val ftueService: FtueService, private val ftueService: FtueService,
private val migrationScreenStore: MigrationScreenStore, private val migrationScreenStore: MigrationScreenStore,
private val pushService: PushService,
) : ClearCacheUseCase { ) : ClearCacheUseCase {
override suspend fun invoke() = withContext(coroutineDispatchers.io) { override suspend fun invoke() = withContext(coroutineDispatchers.io) {
// Clear Matrix cache // Clear Matrix cache
@ -66,5 +68,7 @@ class DefaultClearCacheUseCase @Inject constructor(
migrationScreenStore.reset() migrationScreenStore.reset()
// Ensure the app is restarted // Ensure the app is restarted
defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId) defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId)
// Ensure any error will be displayed again
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
} }
} }

View file

@ -17,8 +17,10 @@
package io.element.android.libraries.push.api package io.element.android.libraries.push.api
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import kotlinx.coroutines.flow.Flow
interface PushService { interface PushService {
/** /**
@ -43,6 +45,9 @@ interface PushService {
distributor: Distributor, distributor: Distributor,
): Result<Unit> ): Result<Unit>
fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean>
suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean)
/** /**
* Return false in case of early error. * Return false in case of early error.
*/ */

View file

@ -19,12 +19,14 @@ package io.element.android.libraries.push.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.impl.test.TestPush import io.element.android.libraries.push.impl.test.TestPush
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import kotlinx.coroutines.flow.Flow
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -72,6 +74,14 @@ class DefaultPushService @Inject constructor(
return pushProvider.registerWith(matrixClient, distributor) return pushProvider.registerWith(matrixClient, distributor)
} }
override fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean> {
return userPushStoreFactory.getOrCreate(sessionId).ignoreRegistrationError()
}
override suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean) {
userPushStoreFactory.getOrCreate(sessionId).setIgnoreRegistrationError(ignore)
}
override suspend fun testPush(): Boolean { override suspend fun testPush(): Boolean {
val pushProvider = getCurrentPushProvider() ?: return false val pushProvider = getCurrentPushProvider() ?: return false
val config = pushProvider.getCurrentUserPushConfig() ?: return false val config = pushProvider.getCurrentUserPushConfig() ?: return false

View file

@ -17,10 +17,13 @@
package io.element.android.libraries.push.test package io.element.android.libraries.push.test
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakePushService( class FakePushService(
private val testPushBlock: suspend () -> Boolean = { true }, private val testPushBlock: suspend () -> Boolean = { true },
@ -53,6 +56,16 @@ class FakePushService(
} }
} }
private val ignoreRegistrationError = MutableStateFlow(false)
override fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean> {
return ignoreRegistrationError
}
override suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean) {
ignoreRegistrationError.value = ignore
}
override suspend fun testPush(): Boolean = simulateLongTask { override suspend fun testPush(): Boolean = simulateLongTask {
testPushBlock() testPushBlock()
} }

View file

@ -29,6 +29,9 @@ interface UserPushStore {
fun getNotificationEnabledForDevice(): Flow<Boolean> fun getNotificationEnabledForDevice(): Flow<Boolean>
suspend fun setNotificationEnabledForDevice(enabled: Boolean) suspend fun setNotificationEnabledForDevice(enabled: Boolean)
fun ignoreRegistrationError(): Flow<Boolean>
suspend fun setIgnoreRegistrationError(ignore: Boolean)
/** /**
* Return true if Pin code is disabled, or if user set the settings to see full notification content. * Return true if Pin code is disabled, or if user set the settings to see full notification content.
*/ */

View file

@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.preferences.preferencesDataStoreFile import androidx.datastore.preferences.preferencesDataStoreFile
import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStore
@ -61,6 +62,7 @@ class UserPushStoreDataStore(
private val pushProviderName = stringPreferencesKey("pushProviderName") private val pushProviderName = stringPreferencesKey("pushProviderName")
private val currentPushKey = stringPreferencesKey("currentPushKey") private val currentPushKey = stringPreferencesKey("currentPushKey")
private val notificationEnabled = booleanPreferencesKey("notificationEnabled") private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
override suspend fun getPushProviderName(): String? { override suspend fun getPushProviderName(): String? {
return context.dataStore.data.first()[pushProviderName] return context.dataStore.data.first()[pushProviderName]
@ -100,6 +102,16 @@ class UserPushStoreDataStore(
return true return true
} }
override fun ignoreRegistrationError(): Flow<Boolean> {
return context.dataStore.data.map { it[ignoreRegistrationError].orFalse() }
}
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
context.dataStore.edit {
it[ignoreRegistrationError] = ignore
}
}
override suspend fun reset() { override suspend fun reset() {
context.dataStore.edit { context.dataStore.edit {
it.clear() it.clear()

View file

@ -25,6 +25,7 @@ class FakeUserPushStore(
) : UserPushStore { ) : UserPushStore {
private var currentRegisteredPushKey: String? = null private var currentRegisteredPushKey: String? = null
private val notificationEnabledForDevice = MutableStateFlow(true) private val notificationEnabledForDevice = MutableStateFlow(true)
private val ignoreRegistrationError = MutableStateFlow(false)
override suspend fun getPushProviderName(): String? { override suspend fun getPushProviderName(): String? {
return pushProviderName return pushProviderName
} }
@ -53,6 +54,14 @@ class FakeUserPushStore(
return true return true
} }
override fun ignoreRegistrationError(): Flow<Boolean> {
return ignoreRegistrationError
}
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
ignoreRegistrationError.value = ignore
}
override suspend fun reset() { override suspend fun reset() {
} }
} }