Merge pull request #3389 from element-hq/feature/bma/removeMigrationScreen
Remove the migration screen
This commit is contained in:
commit
75e232ce17
30 changed files with 9 additions and 444 deletions
|
|
@ -92,7 +92,6 @@ dependencies {
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
testImplementation(projects.features.rageshake.impl)
|
||||||
testImplementation(projects.features.logout.test)
|
testImplementation(projects.features.logout.test)
|
||||||
testImplementation(projects.features.roomlist.test)
|
|
||||||
testImplementation(projects.libraries.indicator.impl)
|
testImplementation(projects.libraries.indicator.impl)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.libraries.fullscreenintent.test)
|
testImplementation(projects.libraries.fullscreenintent.test)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import coil.annotation.ExperimentalCoilApi
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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
|
||||||
|
|
@ -47,7 +46,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
private val defaultCacheService: DefaultCacheService,
|
private val defaultCacheService: DefaultCacheService,
|
||||||
private val okHttpClient: Provider<OkHttpClient>,
|
private val okHttpClient: Provider<OkHttpClient>,
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val migrationScreenStore: MigrationScreenStore,
|
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
) : ClearCacheUseCase {
|
) : ClearCacheUseCase {
|
||||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||||
|
|
@ -64,8 +62,6 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
context.cacheDir.deleteRecursively()
|
context.cacheDir.deleteRecursively()
|
||||||
// Clear some settings
|
// Clear some settings
|
||||||
ftueService.reset()
|
ftueService.reset()
|
||||||
// Clear migration screen store
|
|
||||||
migrationScreenStore.reset()
|
|
||||||
// Ensure any error will be displayed again
|
// Ensure any error will be displayed again
|
||||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
||||||
// Ensure the app is restarted
|
// Ensure the app is restarted
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.ftue.test.FakeFtueService
|
import io.element.android.features.ftue.test.FakeFtueService
|
||||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
|
|
@ -47,10 +46,6 @@ class DefaultClearCacheUseCaseTest {
|
||||||
val ftueService = FakeFtueService(
|
val ftueService = FakeFtueService(
|
||||||
resetLambda = resetFtueLambda,
|
resetLambda = resetFtueLambda,
|
||||||
)
|
)
|
||||||
val resetMigrationLambda = lambdaRecorder<Unit> { }
|
|
||||||
val migrationScreenStore = InMemoryMigrationScreenStore(
|
|
||||||
resetLambda = resetMigrationLambda,
|
|
||||||
)
|
|
||||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||||
val pushService = FakePushService(
|
val pushService = FakePushService(
|
||||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
||||||
|
|
@ -62,14 +57,12 @@ class DefaultClearCacheUseCaseTest {
|
||||||
defaultCacheService = defaultCacheService,
|
defaultCacheService = defaultCacheService,
|
||||||
okHttpClient = { OkHttpClient.Builder().build() },
|
okHttpClient = { OkHttpClient.Builder().build() },
|
||||||
ftueService = ftueService,
|
ftueService = ftueService,
|
||||||
migrationScreenStore = migrationScreenStore,
|
|
||||||
pushService = pushService,
|
pushService = pushService,
|
||||||
)
|
)
|
||||||
defaultCacheService.clearedCacheEventFlow.test {
|
defaultCacheService.clearedCacheEventFlow.test {
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
clearCacheLambda.assertions().isCalledOnce()
|
clearCacheLambda.assertions().isCalledOnce()
|
||||||
resetFtueLambda.assertions().isCalledOnce()
|
resetFtueLambda.assertions().isCalledOnce()
|
||||||
resetMigrationLambda.assertions().isCalledOnce()
|
|
||||||
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
||||||
.with(value(matrixClient.sessionId), value(false))
|
.with(value(matrixClient.sessionId), value(false))
|
||||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.roomlist.api.migration
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
|
|
||||||
interface MigrationScreenStore {
|
|
||||||
fun isMigrationScreenNeeded(sessionId: SessionId): Boolean
|
|
||||||
fun setMigrationScreenShown(sessionId: SessionId)
|
|
||||||
fun reset()
|
|
||||||
}
|
|
||||||
|
|
@ -84,7 +84,6 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.roomlist.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.features.leaveroom.test)
|
testImplementation(projects.features.leaveroom.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
||||||
aRoomsContentState(summaries = persistentListOf()),
|
aRoomsContentState(summaries = persistentListOf()),
|
||||||
aSkeletonContentState(),
|
aSkeletonContentState(),
|
||||||
anEmptyContentState(),
|
anEmptyContentState(),
|
||||||
aMigrationContentState(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,8 +42,6 @@ internal fun aRoomsContentState(
|
||||||
summaries = summaries,
|
summaries = summaries,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aMigrationContentState() = RoomListContentState.Migration
|
|
||||||
|
|
||||||
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||||
|
|
||||||
internal fun anEmptyContentState() = RoomListContentState.Empty
|
internal fun anEmptyContentState() = RoomListContentState.Empty
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
|
@ -93,7 +92,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
||||||
private val searchPresenter: Presenter<RoomListSearchState>,
|
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||||
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
|
@ -210,7 +208,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
|
||||||
}
|
}
|
||||||
val loadingState by roomListDataSource.loadingState.collectAsState()
|
val loadingState by roomListDataSource.loadingState.collectAsState()
|
||||||
val showMigration = migrationScreenPresenter.present().isMigrating
|
|
||||||
val showEmpty by remember {
|
val showEmpty by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
|
||||||
|
|
@ -222,7 +219,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return when {
|
return when {
|
||||||
showMigration -> RoomListContentState.Migration
|
|
||||||
showEmpty -> RoomListContentState.Empty
|
showEmpty -> RoomListContentState.Empty
|
||||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||||
else -> {
|
else -> {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ data class RoomListState(
|
||||||
val eventSink: (RoomListEvents) -> Unit,
|
val eventSink: (RoomListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayFilters = contentState is RoomListContentState.Rooms
|
val displayFilters = contentState is RoomListContentState.Rooms
|
||||||
val displayActions = contentState !is RoomListContentState.Migration
|
val displayActions = true
|
||||||
|
|
||||||
sealed interface ContextMenu {
|
sealed interface ContextMenu {
|
||||||
data object Hidden : ContextMenu
|
data object Hidden : ContextMenu
|
||||||
|
|
@ -72,7 +72,6 @@ enum class SecurityBannerState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface RoomListContentState {
|
sealed interface RoomListContentState {
|
||||||
data object Migration : RoomListContentState
|
|
||||||
data class Skeleton(val count: Int) : RoomListContentState
|
data class Skeleton(val count: Int) : RoomListContentState
|
||||||
data object Empty : RoomListContentState
|
data object Empty : RoomListContentState
|
||||||
data class Rooms(
|
data class Rooms(
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||||
aRoomListState(contentState = anEmptyContentState()),
|
aRoomListState(contentState = anEmptyContentState()),
|
||||||
aRoomListState(contentState = aSkeletonContentState()),
|
aRoomListState(contentState = aSkeletonContentState()),
|
||||||
aRoomListState(matrixUser = MatrixUser(userId = UserId("@id:domain")), contentState = aMigrationContentState()),
|
|
||||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptySta
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
|
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
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
|
||||||
|
|
@ -78,9 +77,6 @@ fun RoomListContentView(
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
when (contentState) {
|
when (contentState) {
|
||||||
is RoomListContentState.Migration -> {
|
|
||||||
MigrationScreenView(isMigrating = true)
|
|
||||||
}
|
|
||||||
is RoomListContentState.Skeleton -> {
|
is RoomListContentState.Skeleton -> {
|
||||||
SkeletonView(
|
SkeletonView(
|
||||||
count = contentState.count,
|
count = contentState.count,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -36,7 +34,4 @@ interface RoomListModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
|
||||||
|
|
||||||
@Binds
|
|
||||||
fun bindMigrationScreenPresenter(presenter: MigrationScreenPresenter): Presenter<MigrationScreenState>
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class MigrationScreenPresenter @Inject constructor(
|
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
private val migrationScreenStore: MigrationScreenStore,
|
|
||||||
) : Presenter<MigrationScreenState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): MigrationScreenState {
|
|
||||||
val roomListState by matrixClient.roomListService.state.collectAsState()
|
|
||||||
var needsMigration by remember { mutableStateOf(migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)) }
|
|
||||||
if (roomListState == RoomListService.State.Running) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
needsMigration = false
|
|
||||||
migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MigrationScreenState(
|
|
||||||
isMigrating = needsMigration && roomListState != RoomListService.State.Running
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
data class MigrationScreenState(
|
|
||||||
val isMigrating: Boolean
|
|
||||||
)
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import io.element.android.features.roomlist.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrationScreenView(
|
|
||||||
isMigrating: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isMigrating,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
label = "Migration view fade",
|
|
||||||
) {
|
|
||||||
SunsetPage(
|
|
||||||
modifier = modifier,
|
|
||||||
isLoading = true,
|
|
||||||
title = stringResource(id = R.string.screen_migration_title),
|
|
||||||
subtitle = stringResource(id = R.string.screen_migration_message),
|
|
||||||
overallContent = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@PreviewsDayNight
|
|
||||||
internal fun MigrationScreenViewPreview() = ElementPreview {
|
|
||||||
MigrationScreenView(isMigrating = true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
|
||||||
import io.element.android.libraries.androidutils.hash.hash
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class SharedPreferencesMigrationScreenStore @Inject constructor(
|
|
||||||
private val sharedPreferences: SharedPreferences,
|
|
||||||
) : MigrationScreenStore {
|
|
||||||
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
|
||||||
return sharedPreferences.getBoolean(sessionId.toKey(), false).not()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setMigrationScreenShown(sessionId: SessionId) {
|
|
||||||
sharedPreferences.edit().putBoolean(sessionId.toKey(), true).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun reset() {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
sharedPreferences.all.keys
|
|
||||||
.filter { it.startsWith(IS_MIGRATION_SCREEN_SHOWN_PREFIX) }
|
|
||||||
.forEach {
|
|
||||||
remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SessionId.toKey(): String {
|
|
||||||
// Hash the sessionId to get rid of exotic char and take only the first 16 chars,
|
|
||||||
// The risk of collision is not high.
|
|
||||||
return IS_MIGRATION_SCREEN_SHOWN_PREFIX + value.hash().take(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val IS_MIGRATION_SCREEN_SHOWN_PREFIX = "is_migration_screen_shown_"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,6 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
|
||||||
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||||
|
|
@ -84,7 +83,6 @@ import io.element.android.libraries.push.test.notifications.FakeNotificationClea
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.MutablePresenter
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
|
@ -483,28 +481,6 @@ class RoomListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - change in migration presenter state modifies contentState`() = runTest {
|
|
||||||
val migrationScreenPresenter = MutablePresenter(MigrationScreenState(true))
|
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
|
||||||
val presenter = createRoomListPresenter(
|
|
||||||
coroutineScope = scope,
|
|
||||||
migrationScreenPresenter = migrationScreenPresenter,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
// The migration screen is shown if the migration screen has not been shown before
|
|
||||||
assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
|
|
||||||
// Set migration as done and set the room list service as running to trigger a refresh of the presenter value
|
|
||||||
migrationScreenPresenter.updateState(MigrationScreenState(false))
|
|
||||||
// The migration screen is not shown anymore
|
|
||||||
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
fun `present - when room service returns no room, then contentState is Empty`() = runTest {
|
||||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
@ -672,7 +648,6 @@ class RoomListPresenterTest {
|
||||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
migrationScreenPresenter: Presenter<MigrationScreenState> = Presenter { MigrationScreenState(false) },
|
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||||
|
|
@ -698,7 +673,6 @@ class RoomListPresenterTest {
|
||||||
sessionVerificationService = client.sessionVerificationService(),
|
sessionVerificationService = client.sessionVerificationService(),
|
||||||
encryptionService = client.encryptionService(),
|
encryptionService = client.encryptionService(),
|
||||||
),
|
),
|
||||||
migrationScreenPresenter = migrationScreenPresenter,
|
|
||||||
searchPresenter = searchPresenter,
|
searchPresenter = searchPresenter,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
filtersPresenter = filtersPresenter,
|
filtersPresenter = filtersPresenter,
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class MigrationScreenPresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial`() = runTest {
|
|
||||||
val presenter = createPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.isMigrating).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - migration end`() = runTest {
|
|
||||||
val matrixClient = FakeMatrixClient()
|
|
||||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
|
||||||
val presenter = createPresenter(matrixClient, migrationScreenStore)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.isMigrating).isTrue()
|
|
||||||
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isTrue()
|
|
||||||
// Simulate room list loaded
|
|
||||||
(matrixClient.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
|
|
||||||
val nextState = awaitItem()
|
|
||||||
assertThat(nextState.isMigrating).isFalse()
|
|
||||||
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse()
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPresenter(
|
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
|
||||||
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
|
||||||
) = MigrationScreenPresenter(
|
|
||||||
matrixClient,
|
|
||||||
migrationScreenStore,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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.
|
|
||||||
*/
|
|
||||||
plugins {
|
|
||||||
id("io.element.android-library")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "io.element.android.features.roomlist.test"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.features.roomlist.api)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
|
||||||
implementation(projects.tests.testutils)
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 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 io.element.android.features.roomlist.impl.migration
|
|
||||||
|
|
||||||
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
|
|
||||||
class InMemoryMigrationScreenStore(
|
|
||||||
private val resetLambda: () -> Unit = { }
|
|
||||||
) : MigrationScreenStore {
|
|
||||||
private val store = mutableMapOf<SessionId, Boolean>()
|
|
||||||
|
|
||||||
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
|
||||||
// If store does not have key return true, else return the opposite of the value
|
|
||||||
return store[sessionId]?.not() ?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setMigrationScreenShown(sessionId: SessionId) {
|
|
||||||
store[sessionId] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun reset() {
|
|
||||||
store.clear()
|
|
||||||
resetLambda()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -30,8 +30,6 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||||
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
|
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
|
|
||||||
import io.element.android.features.roomlist.impl.migration.SharedPreferencesMigrationScreenStore
|
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
|
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
|
||||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
|
@ -120,10 +118,6 @@ class RoomListScreen(
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
),
|
),
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
migrationScreenPresenter = MigrationScreenPresenter(
|
|
||||||
matrixClient = matrixClient,
|
|
||||||
migrationScreenStore = SharedPreferencesMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE))
|
|
||||||
),
|
|
||||||
searchPresenter = RoomListSearchPresenter(
|
searchPresenter = RoomListSearchPresenter(
|
||||||
RoomListSearchDataSource(
|
RoomListSearchDataSource(
|
||||||
roomListService = matrixClient.roomListService,
|
roomListService = matrixClient.roomListService,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:44681362f1416b3957e0f750026a9f21e4d79533d8f2dc0d27a16dca35d61fe4
|
|
||||||
size 125301
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b356d551bbaba92900efb7d2958f5b19b97a7811166e56fd781cb39c041e83f1
|
|
||||||
size 152455
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:44681362f1416b3957e0f750026a9f21e4d79533d8f2dc0d27a16dca35d61fe4
|
|
||||||
size 125301
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b356d551bbaba92900efb7d2958f5b19b97a7811166e56fd781cb39c041e83f1
|
|
||||||
size 152455
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c
|
oid sha256:5870fc3df4bb7330972727601016189e75c358f535f31fe6704837eb866866df
|
||||||
size 5788
|
size 100363
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5870fc3df4bb7330972727601016189e75c358f535f31fe6704837eb866866df
|
|
||||||
size 100363
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b54b82ec6d3bc53463ba18a97149287a32a00c68d466343cdb8988863142097c
|
oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c
|
||||||
size 148172
|
size 5788
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980
|
oid sha256:d236927f6c4cc81f1e9c9b6d405e27e8e96ceb89f9047493b9ab590165d484b2
|
||||||
size 5662
|
size 104246
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d236927f6c4cc81f1e9c9b6d405e27e8e96ceb89f9047493b9ab590165d484b2
|
|
||||||
size 104246
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4c7504bf4b05d74b653c4ad3293d3b917e9ba06779abdf166144aadd74aedb03
|
oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980
|
||||||
size 178957
|
size 5662
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue