From 9379ce2f27319237be400f7942c4c57c170b26e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Tue, 18 Jul 2023 13:35:43 +0200 Subject: [PATCH 1/4] feat: screenshot censoring [WPB-2880] (#1887) * feat: screenshot censoring config [WPB-2880] * add usecases to user session scope * fix updating value after persisting screenshot censoring --- .../configuration/UserConfigRepository.kt | 8 + .../kalium/logic/feature/UserSessionScope.kt | 10 ++ ...ObserveScreenshotCensoringConfigUseCase.kt | 62 +++++++ ...PersistScreenshotCensoringConfigUseCase.kt | 53 ++++++ ...rveScreenshotCensoringConfigUseCaseTest.kt | 169 ++++++++++++++++++ ...istScreenshotCensoringConfigUseCaseTest.kt | 113 ++++++++++++ .../persistence/config/UserConfigStorage.kt | 17 ++ .../config/UserConfigStorageTest.kt | 12 ++ 8 files changed, 444 insertions(+) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/ObserveScreenshotCensoringConfigUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/PersistScreenshotCensoringConfigUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/ObserveScreenshotCensoringConfigUseCaseTest.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/PersistScreenshotCensoringConfigUseCaseTest.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt index a3401feb898..466759b9fc0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt @@ -61,6 +61,8 @@ interface UserConfigRepository { fun setGuestRoomStatus(status: Boolean, isStatusChanged: Boolean?): Either fun getGuestRoomLinkStatus(): Either fun observeGuestRoomLinkFeatureFlag(): Flow> + suspend fun setScreenshotCensoringConfig(enabled: Boolean): Either + suspend fun observeScreenshotCensoringConfig(): Flow> suspend fun getTeamSettingsSelfDeletionStatus(): Either suspend fun setTeamSettingsSelfDeletionStatus( @@ -247,4 +249,10 @@ class UserConfigDataSource( ) } } + + override suspend fun setScreenshotCensoringConfig(enabled: Boolean): Either = + wrapStorageRequest { userConfigStorage.persistScreenshotCensoring(enabled) } + + override suspend fun observeScreenshotCensoringConfig(): Flow> = + userConfigStorage.isScreenshotCensoringEnabledFlow().wrapStorageRequest() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 9df4a130cac..edbf5369d87 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -228,6 +228,10 @@ import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlag import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCaseImpl import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCaseImpl +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCaseImpl +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCaseImpl import com.wire.kalium.logic.feature.user.webSocketStatus.GetPersistentWebSocketStatus import com.wire.kalium.logic.feature.user.webSocketStatus.GetPersistentWebSocketStatusImpl import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase @@ -1350,6 +1354,12 @@ class UserSessionScope internal constructor( val getOtherUserSecurityClassificationLabel: GetOtherUserSecurityClassificationLabelUseCase get() = GetOtherUserSecurityClassificationLabelUseCaseImpl(userConfigRepository) + val persistScreenshotCensoringConfig: PersistScreenshotCensoringConfigUseCase + get() = PersistScreenshotCensoringConfigUseCaseImpl(userConfigRepository = userConfigRepository) + + val observeScreenshotCensoringConfig: ObserveScreenshotCensoringConfigUseCase + get() = ObserveScreenshotCensoringConfigUseCaseImpl(userConfigRepository = userConfigRepository) + val kaliumFileSystem: KaliumFileSystem by lazy { // Create the cache and asset storage directories KaliumFileSystemImpl(dataStoragePaths).also { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/ObserveScreenshotCensoringConfigUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/ObserveScreenshotCensoringConfigUseCase.kt new file mode 100644 index 00000000000..07c1ec3b6c1 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/ObserveScreenshotCensoringConfigUseCase.kt @@ -0,0 +1,62 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.user.screenshotCensoring + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSelfDeleteTimer +import com.wire.kalium.logic.functional.mapRight +import com.wire.kalium.logic.functional.mapToRightOr +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** + * UseCase that allow us to get the configuration of screenshot censoring enabled or not + */ +interface ObserveScreenshotCensoringConfigUseCase { + suspend operator fun invoke(): Flow +} + +internal class ObserveScreenshotCensoringConfigUseCaseImpl( + private val userConfigRepository: UserConfigRepository, +) : ObserveScreenshotCensoringConfigUseCase { + + override suspend fun invoke(): Flow { + return combine( + userConfigRepository.observeScreenshotCensoringConfig() + .mapToRightOr(true), // for safety it's set to true if we can't determine it + userConfigRepository.observeTeamSettingsSelfDeletingStatus() + .mapRight { it.enforcedSelfDeletionTimer is TeamSelfDeleteTimer.Enforced } + .mapToRightOr(true), // for safety it's set to true if we can't determine it + ) { screenshotCensoringEnabled, teamSelfDeletingEnforced -> + when { + teamSelfDeletingEnforced -> ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + screenshotCensoringEnabled -> ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser + else -> ObserveScreenshotCensoringConfigResult.Disabled + } + } + } +} + +sealed class ObserveScreenshotCensoringConfigResult { + object Disabled : ObserveScreenshotCensoringConfigResult() + sealed class Enabled : ObserveScreenshotCensoringConfigResult() { + object ChosenByUser : Enabled() + object EnforcedByTeamSelfDeletingSettings : Enabled() + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/PersistScreenshotCensoringConfigUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/PersistScreenshotCensoringConfigUseCase.kt new file mode 100644 index 00000000000..812e9d93876 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/screenshotCensoring/PersistScreenshotCensoringConfigUseCase.kt @@ -0,0 +1,53 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.user.screenshotCensoring + +import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.LOCAL_STORAGE +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.kaliumLogger + +/** + * UseCase that allow us to persist the configuration of screenshot censoring to enabled or not + */ +interface PersistScreenshotCensoringConfigUseCase { + suspend operator fun invoke(enabled: Boolean): PersistScreenshotCensoringConfigResult +} + +internal class PersistScreenshotCensoringConfigUseCaseImpl( + private val userConfigRepository: UserConfigRepository, +) : PersistScreenshotCensoringConfigUseCase { + + private val logger by lazy { kaliumLogger.withFeatureId(LOCAL_STORAGE) } + + override suspend fun invoke(enabled: Boolean): PersistScreenshotCensoringConfigResult = + userConfigRepository.setScreenshotCensoringConfig(enabled) + .fold({ + logger.e("Failed trying to update screenshot censoring configuration") + PersistScreenshotCensoringConfigResult.Failure(it) + }) { + PersistScreenshotCensoringConfigResult.Success + } +} + +sealed class PersistScreenshotCensoringConfigResult { + object Success : PersistScreenshotCensoringConfigResult() + data class Failure(val cause: CoreFailure) : PersistScreenshotCensoringConfigResult() +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/ObserveScreenshotCensoringConfigUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/ObserveScreenshotCensoringConfigUseCaseTest.kt new file mode 100644 index 00000000000..90f25187b57 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/ObserveScreenshotCensoringConfigUseCaseTest.kt @@ -0,0 +1,169 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.user.screeenshotCensoring + +import app.cash.turbine.test +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSelfDeleteTimer +import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSettingsSelfDeletionStatus +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCaseImpl +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class ObserveScreenshotCensoringConfigUseCaseTest { + + private fun runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult: Either, + observeTeamSelfDeletingStatusResult: Either, + expectedResult: ObserveScreenshotCensoringConfigResult + ) = runTest { + val (arrangement, observeScreenshotCensoringConfig) = Arrangement() + .withObserveScreenshotCensoringConfigResult(observeScreenshotCensoringConfigResult) + .withSuccessfulObserveTeamSelfDeletingStatusResult(observeTeamSelfDeletingStatusResult) + .arrange() + + val result = observeScreenshotCensoringConfig() + + result.test { + val item = awaitItem() + assertTrue { item == expectedResult } + + verify(arrangement.userConfigRepository) + .function(arrangement.userConfigRepository::observeScreenshotCensoringConfig) + .with() + .wasInvoked(once) + + verify(arrangement.userConfigRepository) + .function(arrangement.userConfigRepository::observeTeamSettingsSelfDeletingStatus) + .with() + .wasInvoked(once) + + awaitComplete() + } + } + + @Test + fun givenSSCensoringDisabledAndTeamSelfDeletingNotEnforced_whenInvoking_thenShouldReturnEnabledChosenByUser() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(false), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enabled), + expectedResult = ObserveScreenshotCensoringConfigResult.Disabled + ) + + @Test + fun givenSSCensoringDisabledAndTeamSelfDeletingEnforced_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(false), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enforced(5L.toDuration(DurationUnit.MINUTES))), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + @Test + fun givenSSCensoringDisabledAndTeamSelfDeletingFailure_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(false), + observeTeamSelfDeletingStatusResult = Either.Left(StorageFailure.DataNotFound), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + @Test + fun givenSSCensoringEnabledAndTeamSelfDeletingNotEnforced_whenInvoking_thenShouldReturnEnabledChosenByUser() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(true), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enabled), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser + ) + + @Test + fun givenSSCensoringEnabledAndTeamSelfDeletingEnforced_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(true), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enforced(5L.toDuration(DurationUnit.MINUTES))), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + @Test + fun givenSSCensoringEnabledAndTeamSelfDeletingFailure_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Right(true), + observeTeamSelfDeletingStatusResult = Either.Left(StorageFailure.DataNotFound), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + @Test + fun givenSSCensoringFailureAndTeamSelfDeletingNotEnforced_whenInvoking_thenShouldReturnEnabledChosenByUser() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Left(StorageFailure.DataNotFound), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enabled), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser + ) + + @Test + fun givenSSCensoringFailureAndTeamSelfDeletingEnforced_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Left(StorageFailure.DataNotFound), + observeTeamSelfDeletingStatusResult = Either.Right(TeamSelfDeleteTimer.Enforced(5L.toDuration(DurationUnit.MINUTES))), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + @Test + fun givenSSCensoringFailureAndTeamSelfDeletingFailure_whenInvoking_thenShouldReturnEnabledEnforcedByTeamSelfDeletingSettings() = + runTestWithParametersAndExpectedResult( + observeScreenshotCensoringConfigResult = Either.Left(StorageFailure.DataNotFound), + observeTeamSelfDeletingStatusResult = Either.Left(StorageFailure.DataNotFound), + expectedResult = ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings + ) + + private class Arrangement { + @Mock + val userConfigRepository = mock(classOf()) + + val observeScreenshotCensoringConfig = ObserveScreenshotCensoringConfigUseCaseImpl(userConfigRepository) + + fun withObserveScreenshotCensoringConfigResult(result: Either) = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::observeScreenshotCensoringConfig) + .whenInvoked() + .thenReturn(flowOf(result)) + } + + fun withSuccessfulObserveTeamSelfDeletingStatusResult(result: Either) = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::observeTeamSettingsSelfDeletingStatus) + .whenInvoked() + .thenReturn(flowOf(result.map { TeamSettingsSelfDeletionStatus(false, it) })) + } + + fun arrange() = this to observeScreenshotCensoringConfig + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/PersistScreenshotCensoringConfigUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/PersistScreenshotCensoringConfigUseCaseTest.kt new file mode 100644 index 00000000000..0ed804525f5 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/screeenshotCensoring/PersistScreenshotCensoringConfigUseCaseTest.kt @@ -0,0 +1,113 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.user.screeenshotCensoring + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCaseImpl +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.classOf +import io.mockative.eq +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class PersistScreenshotCensoringConfigUseCaseTest { + + @Test + fun givenATrueValue_shouldCallSetScreenshotCensoringConfigWithTrue() = runTest { + val (arrangement, persistScreenshotCensoringConfig) = Arrangement() + .withSuccessfulCall() + .arrange() + val value = true + val actual = persistScreenshotCensoringConfig(value) + + verify(arrangement.userConfigRepository) + .suspendFunction(arrangement.userConfigRepository::setScreenshotCensoringConfig) + .with(eq(value)) + .wasInvoked(once) + assertTrue(actual is PersistScreenshotCensoringConfigResult.Success) + } + + @Test + fun givenAFalseValue_shouldCallSetScreenshotCensoringConfigWithFalse() = runTest { + val (arrangement, persistScreenshotCensoringConfig) = Arrangement() + .withSuccessfulCall() + .arrange() + val value = false + val actual = persistScreenshotCensoringConfig(value) + + verify(arrangement.userConfigRepository) + .suspendFunction(arrangement.userConfigRepository::setScreenshotCensoringConfig) + .with(eq(value)) + .wasInvoked(once) + + assertTrue(actual is PersistScreenshotCensoringConfigResult.Success) + } + + @Test + fun givenAValue_shouldAndFailsShouldReturnACoreFailureResult() = runTest { + val (arrangement, persistScreenshotCensoringConfig) = Arrangement() + .withFailureToCallRepo() + .arrange() + val value = true + val actual = persistScreenshotCensoringConfig(value) + + verify(arrangement.userConfigRepository) + .suspendFunction(arrangement.userConfigRepository::setScreenshotCensoringConfig) + .with(any()) + .wasInvoked(once) + + assertTrue(actual is PersistScreenshotCensoringConfigResult.Failure) + } + + private class Arrangement { + @Mock + val userConfigRepository = mock(classOf()) + + val persistScreenshotCensoringConfig = PersistScreenshotCensoringConfigUseCaseImpl(userConfigRepository) + + fun withSuccessfulCall() = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::setScreenshotCensoringConfig) + .whenInvokedWith(any()) + .thenReturn(Either.Right(Unit)) + + return this + } + + fun withFailureToCallRepo() = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::setScreenshotCensoringConfig) + .whenInvokedWith(any()) + .thenReturn(Either.Left(StorageFailure.Generic(RuntimeException("Some error")))) + + return this + } + + fun arrange() = this to persistScreenshotCensoringConfig + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt index 00217b0583e..4d855d03350 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt @@ -124,6 +124,8 @@ interface UserConfigStorage { fun persistGuestRoomLinkFeatureFlag(status: Boolean, isStatusChanged: Boolean?) fun isGuestRoomLinkEnabled(): IsGuestRoomLinkEnabledEntity? fun isGuestRoomLinkEnabledFlow(): Flow + fun isScreenshotCensoringEnabledFlow(): Flow + fun persistScreenshotCensoring(enabled: Boolean) } @Serializable @@ -194,6 +196,9 @@ class UserConfigStorageImpl( private val e2EIFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val isScreenshotCensoringEnabledFlow = + MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override fun persistFileSharingStatus( status: Boolean, isStatusChanged: Boolean? @@ -324,6 +329,17 @@ class UserConfigStorageImpl( .onStart { emit(isGuestRoomLinkEnabled()) } .distinctUntilChanged() + override fun isScreenshotCensoringEnabledFlow(): Flow = isScreenshotCensoringEnabledFlow + .map { kaliumPreferences.getBoolean(ENABLE_SCREENSHOT_CENSORING, false) } + .onStart { emit(kaliumPreferences.getBoolean(ENABLE_SCREENSHOT_CENSORING, false)) } + .distinctUntilChanged() + + override fun persistScreenshotCensoring(enabled: Boolean) { + kaliumPreferences.putBoolean(ENABLE_SCREENSHOT_CENSORING, enabled).also { + isScreenshotCensoringEnabledFlow.tryEmit(Unit) + } + } + private companion object { const val FILE_SHARING = "file_sharing" const val GUEST_ROOM_LINK = "guest_room_link" @@ -334,5 +350,6 @@ class UserConfigStorageImpl( const val ENABLE_READ_RECEIPTS = "enable_read_receipts" const val DEFAULT_CONFERENCE_CALLING_ENABLED_VALUE = false const val REQUIRE_SECOND_FACTOR_PASSWORD_CHALLENGE = "require_second_factor_password_challenge" + const val ENABLE_SCREENSHOT_CENSORING = "enable_screenshot_censoring" } } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigStorageTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigStorageTest.kt index 40139e4d099..b3cef06a673 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigStorageTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigStorageTest.kt @@ -127,4 +127,16 @@ class UserConfigStorageTest { assertTrue { it } } } + + @Test + fun givenScreenshotCensoringConfigIsSetToFalse_whenGettingItsValue_thenItShouldBeFalse() = runTest { + userConfigStorage.persistScreenshotCensoring(enabled = false) + assertEquals(false, userConfigStorage.isScreenshotCensoringEnabledFlow().first()) + } + + @Test + fun givenScreenshotCensoringConfigIsSetToTrue_whenGettingItsValue_thenItShouldBeTrue() = runTest { + userConfigStorage.persistScreenshotCensoring(enabled = true) + assertEquals(true, userConfigStorage.isScreenshotCensoringEnabledFlow().first()) + } } From 33aa16c9ad8acbd31ae2efc04da2f5b1852d68aa Mon Sep 17 00:00:00 2001 From: Jacob Persson <7156+typfel@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:00:35 +0200 Subject: [PATCH 2/4] feat: support last_active field on self clients (#1886) * feat: parse & persist last_active field for clients * chore: migration for last_active field * test: update network tests * test: update persistence tests * test: add test for Duration.inWholeWeeks extension * test: update logic tests --- .../kalium/logic/data/client/ClientMapper.kt | 16 +++++++++---- .../kalium/logic/data/client/ClientModel.kt | 12 +++++++++- .../com/wire/kalium/logic/util/CommonUtils.kt | 5 ++++ .../logic/data/client/ClientRepositoryTest.kt | 6 +++++ .../ConversationRepositoryTest.kt | 1 + .../client/GetOrRegisterClientUseCaseTest.kt | 7 +++--- .../client/GetOtherUserClientsUseCaseTest.kt | 23 ++++--------------- .../client/ObserveClientDetailsUseCaseTest.kt | 12 ++-------- .../feature/client/SelfClientsUseCaseTest.kt | 12 ++-------- .../client/VerifyExistingClientUseCaseTest.kt | 5 ++-- .../wire/kalium/logic/framework/TestClient.kt | 1 + .../wire/kalium/logic/util/CommonUtilsTest.kt | 17 ++++++++++++++ .../base/authenticated/client/ClientDTO.kt | 1 + .../wire/kalium/model/ClientResponseJson.kt | 2 ++ .../model/NotificationEventsResponseJson.kt | 2 ++ .../com/wire/kalium/persistence/Clients.sq | 8 ++++--- .../src/commonMain/db_user/migrations/49.sqm | 3 +++ .../persistence/dao/client/ClientDAO.kt | 4 +++- .../persistence/dao/client/ClientDAOImpl.kt | 5 +++- .../wire/kalium/persistence/db/TableMapper.kt | 3 ++- .../dao/UserClientDAOIntegrationTest.kt | 2 ++ .../persistence/dao/client/ClientDAOTest.kt | 9 +++++--- .../dao/newclient/NewClientDAOTest.kt | 3 ++- 23 files changed, 100 insertions(+), 59 deletions(-) create mode 100644 persistence/src/commonMain/db_user/migrations/49.sqm diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientMapper.kt index 5f2115c387e..9ee7cb7ac52 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientMapper.kt @@ -34,6 +34,7 @@ import com.wire.kalium.persistence.dao.client.ClientTypeEntity import com.wire.kalium.persistence.dao.client.DeviceTypeEntity import com.wire.kalium.persistence.dao.client.InsertClientParam import com.wire.kalium.persistence.dao.newclient.NewClientEntity +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import com.wire.kalium.network.api.base.model.UserId as UserIdDTO import com.wire.kalium.persistence.dao.client.Client as ClientEntity @@ -64,6 +65,7 @@ class ClientMapper( id = ClientId(client.clientId), type = fromClientTypeDTO(client.type), registrationTime = Instant.parse(client.registrationTime), + lastActive = client.lastActive?.let { Instant.parse(it).coerceAtMost(Clock.System.now()) }, deviceType = fromDeviceTypeDTO(client.deviceType), label = client.label, model = client.model, @@ -76,6 +78,7 @@ class ClientMapper( id = ClientId(id), type = clientType?.let { fromClientTypeEntity(it) }, registrationTime = registrationDate, + lastActive = lastActive, deviceType = deviceType?.let { fromDeviceTypeEntity(deviceType) }, label = label, model = model, @@ -89,6 +92,7 @@ class ClientMapper( id = ClientId(id), type = null, registrationTime = registrationDate, + lastActive = null, deviceType = deviceType?.let { fromDeviceTypeEntity(deviceType) }, label = null, model = model, @@ -107,7 +111,8 @@ class ClientMapper( clientType = null, label = null, model = null, - registrationDate = null + registrationDate = null, + lastActive = null ) } } @@ -121,7 +126,8 @@ class ClientMapper( clientType = toClientTypeEntity(type), label = label, model = model, - registrationDate = Instant.parse(registrationTime) + registrationDate = Instant.parse(registrationTime), + lastActive = lastActive?.let { Instant.parse(it).coerceAtMost(Clock.System.now()) } ) } @@ -134,7 +140,8 @@ class ClientMapper( clientType = null, label = null, model = null, - registrationDate = null + registrationDate = null, + lastActive = null ) } @@ -146,7 +153,8 @@ class ClientMapper( clientType = event.client.type?.let { toClientTypeEntity(it) }, label = event.client.label, model = event.client.model, - registrationDate = event.client.registrationTime + registrationDate = event.client.registrationTime, + lastActive = event.client.lastActive ) private fun toClientTypeDTO(clientType: ClientType): ClientTypeDTO = when (clientType) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientModel.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientModel.kt index 1f9a3e4d15a..024ca59963c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientModel.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientModel.kt @@ -43,7 +43,17 @@ data class DeleteClientParam( data class Client( val id: ClientId, val type: ClientType?, - val registrationTime: Instant?, // yyyy-mm-ddThh:MM:ss.qqq + /** + * Time when client was registered / created. + */ + val registrationTime: Instant?, + /** + * last time the client was active. + * + * Note that the timestamp has reduced precision, it's only safe + * to operate on the precision of weeks. + * */ + val lastActive: Instant?, val isVerified: Boolean, val isValid: Boolean, val deviceType: DeviceType?, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/CommonUtils.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/CommonUtils.kt index 336d26860d7..473f2932db2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/CommonUtils.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/CommonUtils.kt @@ -23,6 +23,11 @@ import kotlin.contracts.contract import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO +private const val DAYS_IN_WEEK = 7 + +val Duration.inWholeWeeks: Long + get() = inWholeDays / DAYS_IN_WEEK + @OptIn(ExperimentalContracts::class) fun Duration?.isPositiveNotNull(): Boolean { contract { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt index 48fc9bb3776..c91876b035d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt @@ -249,6 +249,7 @@ class ClientRepositoryTest { clientId = "client_id_1", type = ClientTypeDTO.Permanent, registrationTime = "1969-05-12T10:52:02.671Z", + lastActive = "1969-05-12T10:52:02.671Z", deviceType = DeviceTypeDTO.Desktop, label = null, model = "Mac ox", @@ -261,6 +262,7 @@ class ClientRepositoryTest { clientId = "client_id_2", type = ClientTypeDTO.Permanent, registrationTime = "2021-05-12T10:52:02.671Z", + lastActive = "2021-05-12T10:52:02.671Z", deviceType = DeviceTypeDTO.Phone, label = null, model = "iphone 15", @@ -279,6 +281,7 @@ class ClientRepositoryTest { id = PlainId(value = "client_id_1"), type = ClientType.Permanent, registrationTime = Instant.parse("1969-05-12T10:52:02.671Z"), + lastActive = Instant.parse("1969-05-12T10:52:02.671Z"), deviceType = DeviceType.Desktop, label = null, model = "Mac ox", @@ -289,6 +292,7 @@ class ClientRepositoryTest { id = PlainId(value = "client_id_2"), type = ClientType.Permanent, registrationTime = Instant.parse("2021-05-12T10:52:02.671Z"), + lastActive = Instant.parse("2021-05-12T10:52:02.671Z"), deviceType = DeviceType.Phone, label = null, model = "iphone 15", @@ -349,6 +353,7 @@ class ClientRepositoryTest { id = "client-id", clientType = ClientTypeEntity.Permanent, registrationDate = null, + lastActive = null, deviceType = DeviceTypeEntity.Desktop, label = null, model = null, @@ -363,6 +368,7 @@ class ClientRepositoryTest { id = ClientId("client-id"), type = ClientType.Permanent, registrationTime = null, + lastActive = null, deviceType = DeviceType.Desktop, label = null, model = null, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index 25aaf178edd..794315957d5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -916,6 +916,7 @@ class ConversationRepositoryTest { true, null, null, + null, null ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCaseTest.kt index 8db6ab5e1c1..6d0da4b4458 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCaseTest.kt @@ -29,6 +29,7 @@ import com.wire.kalium.logic.data.logout.LogoutRepository import com.wire.kalium.logic.data.notification.PushTokenRepository import com.wire.kalium.logic.feature.CachedClientIdClearer import com.wire.kalium.logic.feature.session.UpgradeCurrentSessionUseCase +import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any @@ -52,7 +53,7 @@ class GetOrRegisterClientUseCaseTest { @Test fun givenValidClientIsRetained_whenRegisteringAClient_thenDoNotRegisterNewAndReturnPersistedClient() = runTest { val clientId = ClientId("clientId") - val client = Client(clientId, ClientType.Permanent, Instant.DISTANT_FUTURE, isVerified = false, isValid = true, null, "label", null) + val client = TestClient.CLIENT.copy(id = clientId) val (arrangement, useCase) = Arrangement() .withRetainedClientIdResult(Either.Right(clientId)) .withVerifyExistingClientResult(VerifyExistingClientResult.Success(client)) @@ -81,7 +82,7 @@ class GetOrRegisterClientUseCaseTest { @Test fun givenInvalidClientIsRetained_whenRegisteringAClient_thenClearDataAndRegisterNewClient() = runTest { val clientId = ClientId("clientId") - val client = Client(clientId, ClientType.Permanent, Instant.DISTANT_FUTURE, isVerified = false, isValid = true, null, "label", null) + val client = TestClient.CLIENT.copy(id = clientId) val (arrangement, useCase) = Arrangement() .withRetainedClientIdResult(Either.Right(clientId)) .withVerifyExistingClientResult(VerifyExistingClientResult.Failure.ClientNotRegistered) @@ -129,7 +130,7 @@ class GetOrRegisterClientUseCaseTest { @Test fun givenClientNotRetained_whenRegisteringAClient_thenRegisterNewClient() = runTest { val clientId = ClientId("clientId") - val client = Client(clientId, ClientType.Permanent, Instant.DISTANT_FUTURE, isVerified = false, isValid = true, null, "label", null) + val client = TestClient.CLIENT.copy(id = clientId) val (arrangement, useCase) = Arrangement() .withRetainedClientIdResult(Either.Left(CoreFailure.MissingClientRegistration)) .withRegisterClientResult(RegisterClientResult.Success(client)) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOtherUserClientsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOtherUserClientsUseCaseTest.kt index 78b762dfdeb..7eed587b844 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOtherUserClientsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/GetOtherUserClientsUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.client.DeviceType import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any @@ -48,25 +49,11 @@ class ObserveClientsByUserIdUseCaseTest { // Given val userId = UserId("123", "wire.com") val clients = listOf( - Client( - id = ClientId("1111"), - type = ClientType.Permanent, - registrationTime = Instant.DISTANT_FUTURE, - deviceType = DeviceType.Desktop, - label = null, - model = "Mac ox", - isVerified = false, - isValid = true + TestClient.CLIENT.copy( + id = ClientId("1111") ), - Client( - id = ClientId("2222"), - type = ClientType.Temporary, - registrationTime = Instant.DISTANT_FUTURE, - deviceType = DeviceType.Phone, - label = null, - model = "Mac ox", - isVerified = false, - isValid = true + TestClient.CLIENT.copy( + id = ClientId("2222") ) ) val (arrangement, getOtherUsersClientsUseCase) = Arrangement() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ObserveClientDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ObserveClientDetailsUseCaseTest.kt index 4b68845c5bb..303ded2067a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ObserveClientDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ObserveClientDetailsUseCaseTest.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.client.DeviceType import com.wire.kalium.logic.data.id.PlainId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.CurrentClientIdProvider +import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.test_util.TestKaliumDispatcher import com.wire.kalium.util.KaliumDispatcher @@ -95,16 +96,7 @@ class ObserveClientDetailsUseCaseTest { private companion object { val USER_ID = UserId("user_id", "domain") val CLIENT_ID = PlainId(value = "client_id_1") - val CLIENT = Client( - id = CLIENT_ID, - type = ClientType.Permanent, - registrationTime = Instant.DISTANT_FUTURE, - deviceType = DeviceType.Desktop, - label = null, - model = "Mac ox", - isVerified = false, - isValid = true - ) + val CLIENT = TestClient.CLIENT val CLIENT_RESULT = CLIENT.copy(id = PlainId(value = "client_id_1")) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/SelfClientsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/SelfClientsUseCaseTest.kt index 5ccb5eef5e3..aa178daeb8e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/SelfClientsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/SelfClientsUseCaseTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.client.DeviceType import com.wire.kalium.logic.data.id.PlainId import com.wire.kalium.logic.feature.CurrentClientIdProvider +import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import com.wire.kalium.network.exceptions.KaliumException import io.ktor.utils.io.errors.IOException @@ -102,16 +103,7 @@ class SelfClientsUseCaseTest { } private companion object { - val CLIENT = Client( - id = PlainId(value = "client_id_1"), - type = ClientType.Permanent, - registrationTime = Instant.parse("2022-01-01T10:52:02.671Z"), - deviceType = DeviceType.Desktop, - label = null, - model = "Mac ox", - isVerified = false, - isValid = true - ) + val CLIENT = TestClient.CLIENT val CLIENTS_LIST = listOf( CLIENT.copy(id = PlainId(value = "client_id_1")), CLIENT.copy(id = PlainId(value = "client_id_2")) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt index 9421b650dfa..dfb62f1da3a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt @@ -22,8 +22,8 @@ package com.wire.kalium.logic.feature.client import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.client.Client import com.wire.kalium.logic.data.client.ClientRepository -import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any @@ -33,7 +33,6 @@ import io.mockative.mock import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -44,7 +43,7 @@ class VerifyExistingClientUseCaseTest { @Test fun givenRegisteredClientId_whenInvoking_thenReturnSuccess() = runTest { val clientId = ClientId("clientId") - val client = Client(clientId, ClientType.Permanent, Instant.DISTANT_PAST, isVerified = false, isValid = false, null, null, "label") + val client = TestClient.CLIENT.copy(id = clientId) val (_, useCase) = Arrangement() .withSelfClientsResult(Either.Right(listOf(client))) .arrange() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestClient.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestClient.kt index d11f39b3218..6badcf77c32 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestClient.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestClient.kt @@ -32,6 +32,7 @@ object TestClient { CLIENT_ID, ClientType.Permanent, Instant.DISTANT_PAST, + Instant.DISTANT_PAST, deviceType = null, model = null, label = "label", diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/CommonUtilsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/CommonUtilsTest.kt index 44fc107b5a6..0cc4daf6b32 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/CommonUtilsTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/CommonUtilsTest.kt @@ -18,11 +18,28 @@ package com.wire.kalium.logic.util +import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.days class CommonUtilsTest { + @Test + fun givenDurationOfLessThanAWeek_thenInWholeWeeksReturnsZero() { + assertEquals(0, 6.days.inWholeWeeks) + } + + @Test + fun givenDurationOfMoreThanAWeek_thenInWholeWeeksReturnsOne() { + assertEquals(1, 7.days.inWholeWeeks) + } + + @Test + fun givenDurationOfMoreThanTwoWeeks_thenInWholeWeeksReturnsTwo() { + assertEquals(2, 15.days.inWholeWeeks) + } + @Test fun givenAFileName_whenGettingItsFileExtension_itReturnsItCorrectly() { val fileName = "some_dummy_image_file.jpg" diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/client/ClientDTO.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/client/ClientDTO.kt index 44f11480271..afafd3b9884 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/client/ClientDTO.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/client/ClientDTO.kt @@ -26,6 +26,7 @@ import kotlinx.serialization.Serializable data class ClientDTO( @SerialName("cookie") val cookie: String?, @SerialName("time") val registrationTime: String, // yyyy-mm-ddThh:MM:ss.qqq + @SerialName("last_active") val lastActive: String?, // yyyy-mm-ddThh:MM:ss.qqq @SerialName("location") val location: LocationResponse?, @SerialName("model") val model: String?, @SerialName("id") val clientId: String, diff --git a/network/src/commonTest/kotlin/com/wire/kalium/model/ClientResponseJson.kt b/network/src/commonTest/kotlin/com/wire/kalium/model/ClientResponseJson.kt index 37962a7a6e2..adacc673abb 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/model/ClientResponseJson.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/model/ClientResponseJson.kt @@ -33,6 +33,7 @@ object ClientResponseJson { | "id": "${serializable.clientId}", | "type": "${serializable.type}", | "time": "${serializable.registrationTime}", + | "last_active": "${serializable.lastActive}", | "class": "${serializable.deviceType}", | "label": "${serializable.label}", | "cookie": "${serializable.cookie}", @@ -56,6 +57,7 @@ object ClientResponseJson { type = ClientTypeDTO.Permanent, deviceType = DeviceTypeDTO.Phone, registrationTime = "2021-05-12T10:52:02.671Z", + lastActive = "2021-05-12T10:52:02.671Z", location = LocationResponse(latitude = "1.2345", longitude = "6.7890"), label = "label", cookie = "sldkfmdeklmwldwlek23kl44mntiuepfojfndkjd", diff --git a/network/src/commonTest/kotlin/com/wire/kalium/model/NotificationEventsResponseJson.kt b/network/src/commonTest/kotlin/com/wire/kalium/model/NotificationEventsResponseJson.kt index 3f31a6cadfc..154ecb7090f 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/model/NotificationEventsResponseJson.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/model/NotificationEventsResponseJson.kt @@ -50,6 +50,7 @@ object NotificationEventsResponseJson { | "type": "user.client-add", | "client": { | "time": "${eventData.client.registrationTime}", + | "last_active": "${eventData.client.lastActive}", | "model": "${eventData.client.model}", | "id": "71ff8872e468a970", | "type": "${eventData.client.type}", @@ -71,6 +72,7 @@ object NotificationEventsResponseJson { clientId = "id", location = LocationResponse("23.2", "43.2"), registrationTime = "2022-02-15T12:54:30Z", + lastActive = "2022-02-15T12:54:30Z", model = "Firefox (Temporary)", type = ClientTypeDTO.Permanent, deviceType = DeviceTypeDTO.Desktop, diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq index f5c7eb6b5ba..b7644720747 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Clients.sq @@ -14,6 +14,7 @@ CREATE TABLE Client ( registration_date INTEGER AS Instant DEFAULT NULL, label TEXT DEFAULT NULL, model TEXT DEFAULT NULL, + last_active INTEGER AS Instant DEFAULT NULL, FOREIGN KEY (user_id) REFERENCES User(qualified_id) ON DELETE CASCADE, PRIMARY KEY (user_id, id) ); @@ -28,14 +29,15 @@ deleteClientsOfUser: DELETE FROM Client WHERE user_id = ?; insertClient: -INSERT INTO Client(user_id, id,device_type, client_type, is_valid, registration_date, label, model) -VALUES(?, ?,?, ?, ?,?, ?, ?) +INSERT INTO Client(user_id, id,device_type, client_type, is_valid, registration_date, label, model, last_active) +VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id, user_id) DO UPDATE SET device_type = coalesce(excluded.device_type, device_type), registration_date = coalesce(excluded.registration_date, registration_date), label = coalesce(excluded.label, label), model = coalesce(excluded.model, model), -is_valid = is_valid; +is_valid = is_valid, +last_active = coalesce(excluded.last_active, last_active); selectAllClients: SELECT * FROM Client; diff --git a/persistence/src/commonMain/db_user/migrations/49.sqm b/persistence/src/commonMain/db_user/migrations/49.sqm new file mode 100644 index 00000000000..96452232b64 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/49.sqm @@ -0,0 +1,3 @@ +import kotlinx.datetime.Instant; + +ALTER TABLE Client ADD COLUMN last_active INTEGER AS Instant DEFAULT NULL; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt index ef630b0474c..a8fcff8957a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAO.kt @@ -31,6 +31,7 @@ data class Client( val isValid: Boolean, val isVerified: Boolean, val registrationDate: Instant?, + val lastActive: Instant?, val label: String?, val model: String?, ) @@ -42,7 +43,8 @@ data class InsertClientParam( val clientType: ClientTypeEntity?, val label: String?, val registrationDate: Instant?, - val model: String? + val lastActive: Instant?, + val model: String?, ) enum class DeviceTypeEntity { diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt index 556e1bcea70..88ab00bb814 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt @@ -42,6 +42,7 @@ internal object ClientMapper { registration_date: Instant?, label: String?, model: String?, + lastActive: Instant? ): Client = Client( userId = user_id, id = id, @@ -51,7 +52,8 @@ internal object ClientMapper { isVerified = is_verified, registrationDate = registration_date, label = label, - model = model + model = model, + lastActive = lastActive ) } @@ -79,6 +81,7 @@ internal class ClientDAOImpl internal constructor( client_type = clientType, is_valid = true, registration_date = registrationDate, + last_active = lastActive, model = model, label = label ) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index 894d5a836eb..66aa0da6c62 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -67,7 +67,8 @@ internal object TableMapper { user_idAdapter = QualifiedIDAdapter, device_typeAdapter = EnumColumnAdapter(), client_typeAdapter = EnumColumnAdapter(), - registration_dateAdapter = InstantTypeAdapter + registration_dateAdapter = InstantTypeAdapter, + last_activeAdapter = InstantTypeAdapter ) val connectionAdapter = Connection.Adapter( qualified_conversationAdapter = QualifiedIDAdapter, diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserClientDAOIntegrationTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserClientDAOIntegrationTest.kt index 5c510d71125..a52ef4a53ee 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserClientDAOIntegrationTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserClientDAOIntegrationTest.kt @@ -73,6 +73,7 @@ class UserClientDAOIntegrationTest : BaseDatabaseTest() { isValid = true, isVerified = false, registrationDate = null, + lastActive = null, label = null, clientType = null, model = null @@ -84,6 +85,7 @@ class UserClientDAOIntegrationTest : BaseDatabaseTest() { client.clientType, client.label, client.registrationDate, + client.lastActive, client.model ) } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt index e4d4b9eb50a..e3c1ad6d50d 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt @@ -314,7 +314,8 @@ class ClientDAOTest : BaseDatabaseTest() { clientType = null, label = null, model = null, - registrationDate = null + registrationDate = null, + lastActive = null ) clientDAO.insertClient(insertedClient2) @@ -336,7 +337,8 @@ class ClientDAOTest : BaseDatabaseTest() { clientType = null, label = null, model = null, - registrationDate = null + registrationDate = null, + lastActive = null ) val client = insertedClient.toClient() @@ -377,5 +379,6 @@ private fun InsertClientParam.toClient(): Client = isVerified = false, label = label, model = model, - registrationDate = registrationDate + registrationDate = registrationDate, + lastActive = lastActive ) diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOTest.kt index 87ce583cf8a..68d20ff616d 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOTest.kt @@ -73,7 +73,8 @@ class NewClientDAOTest: BaseDatabaseTest() { clientType = null, label = null, model = null, - registrationDate = null + registrationDate = null, + lastActive = null ) val insertedClient1 = insertedClient.copy(user.id, "id1", deviceType = null) From 48b43cf55285f00c8ef0b03e2a74db1b6bff0bba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:12:55 +0200 Subject: [PATCH 3/4] ci: automatically cherry-pick changes from release/candidate (#1894) (#1895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: (cherry picked from commit 61695e3c4a9bfe394e2d1a219b4595740e62d9f6) Co-authored-by: Vitor Hugo Schwaab --- .../workflows/cherry-pick-rc-to-develop.yml | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/cherry-pick-rc-to-develop.yml diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml new file mode 100644 index 00000000000..6ceccf09a49 --- /dev/null +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -0,0 +1,50 @@ +name: "Cherry-pick from rc to develop" + +on: + pull_request: + branches: + - release/candidate + types: + - closed + +jobs: + cherry-pick: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Append -cherry-pick to branch name + id: extract + run: | + PR_BRANCH="${{ github.event.pull_request.head.ref }}" + NEW_BRANCH_NAME="${PR_BRANCH}-cherry-pick" + echo "New branch name: $NEW_BRANCH_NAME" + echo "::set-output name=newBranchName::$NEW_BRANCH_NAME" + + - uses: fregante/setup-git-user@v2 + + - name: Cherry-pick commits + run: | + git fetch origin develop:develop + git checkout -b ${{ steps.extract.outputs.newBranchName }} develop + # Cherry-picking the last commit on the base branch + git cherry-pick -x ${{ github.event.pull_request.merge_commit_sha }} --strategy-option theirs || true + git add . + git cherry-pick --continue || true + git push origin ${{ steps.extract.outputs.newBranchName }} + + - name: Create PR + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BRANCH: ${{ steps.extract.outputs.newBranchName }} + PR_ASSIGNEE: ${{ github.event.pull_request.user.login }} + PR_BODY: "${{ format('Cherry pick from the original PR: \n- #{0}\n\n ---- \n{1}', github.event.pull_request.number, github.event.pull_request.body) }}" + run: gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base develop --head $PR_BRANCH --label "cherry-pick" --assignee "$PR_ASSIGNEE" From 862961480638b9709d8add442a097b2f13683cdc Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Tue, 18 Jul 2023 17:16:55 +0200 Subject: [PATCH 4/4] ci: remove -x argument from cherry-pick The commit message already holds reference to the original PR, and this gets in the way of giving credit to all the people involved by breaking the `Co-Authored By` block. --- .github/workflows/cherry-pick-rc-to-develop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml index 6ceccf09a49..7755fff6f51 100644 --- a/.github/workflows/cherry-pick-rc-to-develop.yml +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -36,7 +36,7 @@ jobs: git fetch origin develop:develop git checkout -b ${{ steps.extract.outputs.newBranchName }} develop # Cherry-picking the last commit on the base branch - git cherry-pick -x ${{ github.event.pull_request.merge_commit_sha }} --strategy-option theirs || true + git cherry-pick ${{ github.event.pull_request.merge_commit_sha }} --strategy-option theirs || true git add . git cherry-pick --continue || true git push origin ${{ steps.extract.outputs.newBranchName }}