Merge pull request #7694 from vector-im/feature/ons/unverified_sessions_alert

Remind unverified sessions with a banner once a week (PSG-892)
This commit is contained in:
Onuray Sahin 2022-12-03 14:23:10 +03:00 committed by GitHub
commit 34d29dc9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 280 additions and 53 deletions

1
changelog.d/7694.feature Normal file
View File

@ -0,0 +1 @@
Remind unverified sessions with a banner once a week

View File

@ -2649,8 +2649,12 @@
<string name="unencrypted">Unencrypted</string> <string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string> <string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string> <string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string>
<string name="review_logins">Review where youre logged in</string> <!-- TODO TO BE REMOVED -->
<string name="verify_other_sessions">Verify all your sessions to ensure your account &amp; messages are safe</string> <string name="review_logins" tools:ignore="UnusedResources">Review where youre logged in</string>
<!-- TODO TO BE REMOVED -->
<string name="verify_other_sessions" tools:ignore="UnusedResources">Verify all your sessions to ensure your account &amp; messages are safe</string>
<string name="review_unverified_sessions_title">You have unverified sessions</string>
<string name="review_unverified_sessions_description">Review to ensure your account is safe</string>
<!-- Argument will be replaced by the other session name (e.g, Desktop, mobile) --> <!-- Argument will be replaced by the other session name (e.g, Desktop, mobile) -->
<string name="verify_this_session">Verify the new login accessing your account: %1$s</string> <string name="verify_this_session">Verify the new login accessing your account: %1$s</string>

View File

@ -88,6 +88,9 @@ class DebugVectorFeatures(
override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled)
?: vectorFeatures.isVoiceBroadcastEnabled() ?: vectorFeatures.isVoiceBroadcastEnabled()
override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled)
?: vectorFeatures.isUnverifiedSessionsAlertEnabled()
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences { fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
if (value == null) { if (value == null) {
it.remove(key) it.remove(key)
@ -151,4 +154,5 @@ object DebugFeatureKeys {
val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers") val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers")
val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login") val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login")
val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled")
val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled")
} }

View File

@ -16,6 +16,8 @@
package im.vector.app.config package im.vector.app.config
import kotlin.time.Duration.Companion.days
/** /**
* Set of flags to configure the application. * Set of flags to configure the application.
*/ */
@ -93,4 +95,6 @@ object Config {
* Can be disabled by providing Analytics.Disabled * Can be disabled by providing Analytics.Disabled
*/ */
val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY")
val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week
} }

View File

@ -44,6 +44,7 @@ interface VectorFeatures {
fun isQrCodeLoginForAllServers(): Boolean fun isQrCodeLoginForAllServers(): Boolean
fun isReciprocateQrCodeLogin(): Boolean fun isReciprocateQrCodeLogin(): Boolean
fun isVoiceBroadcastEnabled(): Boolean fun isVoiceBroadcastEnabled(): Boolean
fun isUnverifiedSessionsAlertEnabled(): Boolean
} }
class DefaultVectorFeatures : VectorFeatures { class DefaultVectorFeatures : VectorFeatures {
@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isQrCodeLoginForAllServers(): Boolean = false override fun isQrCodeLoginForAllServers(): Boolean = false
override fun isReciprocateQrCodeLogin(): Boolean = false override fun isReciprocateQrCodeLogin(): Boolean = false
override fun isVoiceBroadcastEnabled(): Boolean = true override fun isVoiceBroadcastEnabled(): Boolean = true
override fun isUnverifiedSessionsAlertEnabled(): Boolean = true
} }

View File

@ -239,12 +239,12 @@ class HomeDetailFragment :
.requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
} }
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
dismissedAction = Runnable { dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
} }
@ -256,8 +256,8 @@ class HomeDetailFragment :
alertManager.postVectorAlert( alertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(
uid = uid, uid = uid,
title = getString(R.string.review_logins), title = getString(R.string.review_unverified_sessions_title),
description = getString(R.string.verify_other_sessions), description = getString(R.string.review_unverified_sessions_description),
iconId = R.drawable.ic_shield_warning iconId = R.drawable.ic_shield_warning
).apply { ).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 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.home
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class IsNewLoginAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
) {
fun execute(deviceId: String?): Boolean {
deviceId ?: return false
return vectorPreferences.isNewLoginAlertShownForDevice(deviceId)
}
}

View File

@ -253,12 +253,12 @@ class NewHomeDetailFragment :
.requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
} }
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
dismissedAction = Runnable { dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
} }
@ -270,8 +270,8 @@ class NewHomeDetailFragment :
alertManager.postVectorAlert( alertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(
uid = uid, uid = uid,
title = getString(R.string.review_logins), title = getString(R.string.review_unverified_sessions_title),
description = getString(R.string.verify_other_sessions), description = getString(R.string.review_unverified_sessions_description),
iconId = R.drawable.ic_shield_warning iconId = R.drawable.ic_shield_warning
).apply { ).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 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.home
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class SetNewLoginAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
) {
fun execute(deviceIds: List<String>) {
deviceIds.forEach {
vectorPreferences.setNewLoginAlertShownForDevice(it)
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 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.home
import im.vector.app.core.time.Clock
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class SetUnverifiedSessionsAlertShownUseCase @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val clock: Clock,
) {
fun execute(deviceIds: List<String>) {
val epochMillis = clock.epochMillis()
deviceIds.forEach {
vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis)
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 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.home
import im.vector.app.config.Config
import im.vector.app.core.time.Clock
import im.vector.app.features.VectorFeatures
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor(
private val vectorFeatures: VectorFeatures,
private val vectorPreferences: VectorPreferences,
private val clock: Clock,
) {
fun execute(deviceId: String?): Boolean {
deviceId ?: return false
val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled()
val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId)
return isUnverifiedSessionsAlertEnabled &&
clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
}
}

View File

@ -19,7 +19,6 @@ package im.vector.app.features.home
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -63,12 +61,16 @@ data class DeviceDetectionInfo(
class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
@Assisted initialState: UnknownDevicesState, @Assisted initialState: UnknownDevicesState,
session: Session, session: Session,
private val vectorPreferences: VectorPreferences,
clock: Clock, clock: Clock,
private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase,
private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) { ) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction { sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceIds: List<String>) : Action() data class IgnoreDevice(val deviceIds: List<String>) : Action()
data class IgnoreNewLogin(val deviceIds: List<String>) : Action()
} }
@AssistedFactory @AssistedFactory
@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
} }
} }
private val ignoredDeviceList = ArrayList<String>()
init { init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId)
.firstOrNull { it.deviceId == session.sessionParams.deviceId } .firstOrNull { it.deviceId == session.sessionParams.deviceId }
@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
?: clock.epochMillis() ?: clock.epochMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs") Timber.v("## Detector - Current Session first time seen $currentSessionTs")
ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it")
}
)
combine( combine(
session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo(), session.flow().liveMyDevicesInfo(),
@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
} }
// filter out ignored devices // filter out ignored devices
.filter { !ignoredDeviceList.contains(it.deviceId) } .filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) }
.sortedByDescending { it.lastSeenTs } .sortedByDescending { it.lastSeenTs }
.map { deviceInfo -> .map { deviceInfo ->
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs
DeviceDetectionInfo( DeviceDetectionInfo(
deviceInfo, deviceInfo,
deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, isNew,
pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
) )
} }
@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
override fun handle(action: Action) { override fun handle(action: Action) {
when (action) { when (action) {
is Action.IgnoreDevice -> { is Action.IgnoreDevice -> {
ignoredDeviceList.addAll(action.deviceIds) setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds)
// local echo }
withState { state -> is Action.IgnoreNewLogin -> {
state.unknownSessions.invoke()?.let { detectedSessions -> setNewLoginAlertShownUseCase.execute(action.deviceIds)
val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
setState {
copy(unknownSessions = Success(updated))
}
}
}
} }
} }
} }
override fun onCleared() {
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
super.onCleared()
}
} }

View File

@ -228,8 +228,6 @@ class VectorPreferences @Inject constructor(
private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_1_MONTH = 2
private const val MEDIA_SAVING_FOREVER = 3 private const val MEDIA_SAVING_FOREVER = 3
private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION"
@ -246,6 +244,9 @@ class VectorPreferences @Inject constructor(
// This key will be used to enable user for displaying live user info or not. // This key will be used to enable user for displaying live user info or not.
const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_"
const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_"
// Possible values for TAKE_PHOTO_VIDEO_MODE // Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
@ -521,18 +522,6 @@ class VectorPreferences @Inject constructor(
return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
} }
fun storeUnknownDeviceDismissedList(deviceIds: List<String>) {
defaultPrefs.edit(true) {
putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet())
}
}
fun getUnknownDeviceDismissedList(): List<String> {
return tryOrNull {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
}.orEmpty()
}
/** /**
* Update the notification ringtone. * Update the notification ringtone.
* *
@ -1243,7 +1232,27 @@ class VectorPreferences @Inject constructor(
fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) {
defaultPrefs.edit { defaultPrefs.edit {
putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible)
}
}
fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long {
return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0)
}
fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) {
defaultPrefs.edit {
putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis)
}
}
fun isNewLoginAlertShownForDevice(deviceId: String): Boolean {
return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false)
}
fun setNewLoginAlertShownForDevice(deviceId: String) {
defaultPrefs.edit {
putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true)
} }
} }
} }

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2022 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.home
import im.vector.app.config.Config
import im.vector.app.test.fakes.FakeClock
import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorPreferences
import org.amshove.kluent.shouldBe
import org.junit.Test
private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS.toLong()
private const val A_DEVICE_ID = "A_DEVICE_ID"
class ShouldShowUnverifiedSessionsAlertUseCaseTest {
private val fakeVectorFeatures = FakeVectorFeatures()
private val fakeVectorPreferences = FakeVectorPreferences()
private val fakeClock = FakeClock()
private val shouldShowUnverifiedSessionsAlertUseCase = ShouldShowUnverifiedSessionsAlertUseCase(
vectorFeatures = fakeVectorFeatures,
vectorPreferences = fakeVectorPreferences.instance,
clock = fakeClock,
)
@Test
fun `given the feature is disabled then the use case returns false`() {
fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false)
fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false
}
@Test
fun `given the feature in enabled and there is not a saved preference then the use case returns true`() {
fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
fakeClock.givenEpoch(AN_EPOCH + 1)
shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true
}
@Test
fun `given the feature in enabled and last shown is a long time ago then the use case returns true`() {
fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
fakeClock.givenEpoch(AN_EPOCH * 2 + 1)
shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true
}
@Test
fun `given the feature in enabled and last shown is not a long time ago then the use case returns false`() {
fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
fakeClock.givenEpoch(AN_EPOCH + 1)
shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false
}
}

View File

@ -50,4 +50,8 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
fun givenVoiceBroadcast(isEnabled: Boolean) { fun givenVoiceBroadcast(isEnabled: Boolean) {
every { isVoiceBroadcastEnabled() } returns isEnabled every { isVoiceBroadcastEnabled() } returns isEnabled
} }
fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) {
every { isUnverifiedSessionsAlertEnabled() } returns isEnabled
}
} }

View File

@ -56,4 +56,8 @@ class FakeVectorPreferences {
fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) {
every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress
} }
fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis
}
} }