Show blocked users list (#2437)
* Show blocked users list. Also allow to unblock them from this list. * Add non-blocking `AsyncIndicatorHost` component * Use `StateFlow` for getting ignored users. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
1fd78f2e69
commit
cdf89adcd2
108 changed files with 1334 additions and 106 deletions
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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.libraries.designsystem.components.async
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
||||
/**
|
||||
* A helper to create [AsyncIndicatorView] with some defaults.
|
||||
*/
|
||||
@Stable
|
||||
object AsyncIndicator {
|
||||
/**
|
||||
* A loading async indicator.
|
||||
* @param text The text to display.
|
||||
* @param modifier The modifier to apply to the indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun Loading(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AsyncIndicatorView(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
spacing = 10.dp,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
.size(12.dp),
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
strokeWidth = 1.5.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A failure async indicator.
|
||||
* @param text The text to display.
|
||||
* @param modifier The modifier to apply to the indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun Failure(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AsyncIndicatorView(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
spacing = defaultSpacing
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(18.dp),
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom async indicator.
|
||||
* @param text The text to display.
|
||||
* @param modifier The modifier to apply to the indicator.
|
||||
* @param spacing The spacing between the leading content and the text.
|
||||
* @param leadingContent The leading content to display.
|
||||
*/
|
||||
@Composable
|
||||
fun Custom(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
spacing: Dp = defaultSpacing,
|
||||
leadingContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
AsyncIndicatorView(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
spacing = spacing,
|
||||
leadingContent = leadingContent,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A short duration to display indicators.
|
||||
*/
|
||||
const val DURATION_SHORT = 3000L
|
||||
|
||||
/**
|
||||
* A long duration to display indicators.
|
||||
*/
|
||||
const val DURATION_LONG = 5000L
|
||||
|
||||
private val defaultSpacing = 4.dp
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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.libraries.designsystem.components.async
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class AsyncIndicatorState {
|
||||
private val queue = SnapshotStateList<AsyncIndicatorItem>()
|
||||
val currentItem = mutableStateOf<AsyncIndicatorItem?>(null)
|
||||
val currentAnimationState = MutableTransitionState(false)
|
||||
|
||||
/**
|
||||
* Enqueue a new indicator to be displayed.
|
||||
* @param durationMs The duration to display the indicator, if `null` (the default value) it will be displayed indefinitely, until the next indicator is
|
||||
* displayed or the current one is manually cleared.
|
||||
* @param composable The composable to display.
|
||||
*/
|
||||
fun enqueue(durationMs: Long? = null, composable: @Composable () -> Unit) {
|
||||
queue.add(AsyncIndicatorItem(composable, durationMs))
|
||||
if (currentItem.value == null || currentItem.value?.durationMs == null) {
|
||||
nextState()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun nextState() {
|
||||
if (!currentAnimationState.isIdle) return
|
||||
|
||||
if (currentItem.value != null && currentAnimationState.currentState && currentAnimationState.isIdle) {
|
||||
// Is visible and not animating, start the exit animation
|
||||
currentAnimationState.targetState = false
|
||||
} else if (currentItem.value == null || !currentAnimationState.currentState && currentAnimationState.isIdle) {
|
||||
// Not visible or present, start the enter animation for the next item
|
||||
val newItem = queue.removeFirstOrNull()
|
||||
if (newItem != null) {
|
||||
currentItem.value = null
|
||||
currentAnimationState.targetState = true
|
||||
}
|
||||
currentItem.value = newItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current indicator using its exit animation.
|
||||
*/
|
||||
fun clear() {
|
||||
currentAnimationState.targetState = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An item to be displayed in the [AsyncIndicatorHost].
|
||||
*/
|
||||
data class AsyncIndicatorItem(
|
||||
val composable: @Composable () -> Unit,
|
||||
val durationMs: Long? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Remember an [AsyncIndicatorState] instance.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberAsyncIndicatorState(): AsyncIndicatorState {
|
||||
return remember { AsyncIndicatorState() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A host for displaying async indicators.
|
||||
* @param modifier The modifier to apply.
|
||||
* @param state The [AsyncIndicatorState] which values this component will display.
|
||||
* @param enterTransition The enter transition to use for the displayed indicators.
|
||||
* @param exitTransition The exit transition to use for the hiding indicators.
|
||||
*/
|
||||
@Composable
|
||||
fun AsyncIndicatorHost(
|
||||
modifier: Modifier = Modifier,
|
||||
state: AsyncIndicatorState = rememberAsyncIndicatorState(),
|
||||
enterTransition: EnterTransition = fadeIn(spring(stiffness = 500F)) + slideInVertically(),
|
||||
exitTransition: ExitTransition = fadeOut(spring(stiffness = 500F)) + slideOutVertically(),
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
state.currentItem.value?.composable?.invoke()
|
||||
} else {
|
||||
state.currentItem.value?.let { item ->
|
||||
AnimatedVisibility(
|
||||
visibleState = state.currentAnimationState,
|
||||
enter = enterTransition,
|
||||
exit = exitTransition,
|
||||
) {
|
||||
item.composable()
|
||||
}
|
||||
|
||||
if (state.currentAnimationState.hasEntered() && item.durationMs != null) {
|
||||
SideEffect {
|
||||
coroutineScope.launch {
|
||||
delay(item.durationMs)
|
||||
state.nextState()
|
||||
}
|
||||
}
|
||||
} else if (state.currentAnimationState.hasExited()) {
|
||||
SideEffect {
|
||||
state.nextState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MutableTransitionState<Boolean>.hasEntered() = currentState && isIdle
|
||||
internal fun MutableTransitionState<Boolean>.hasExited() = !currentState && isIdle
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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.libraries.designsystem.components.async
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
internal fun AsyncIndicatorView(
|
||||
text: String,
|
||||
spacing: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
elevation: Dp = 8.dp,
|
||||
leadingContent: @Composable (() -> Unit)?,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(elevation)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
shadowElevation = elevation,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(color = ElementTheme.colors.bgSubtleSecondary)
|
||||
.padding(horizontal = 24.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
leadingContent?.let { view ->
|
||||
view()
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AsyncIndicatorView_Loading_Preview() {
|
||||
ElementPreview {
|
||||
AsyncIndicator.Loading(text = "Loading")
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AsyncIndicatorView_Failed_Preview() {
|
||||
ElementPreview {
|
||||
AsyncIndicator.Failure(text = "Failed")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* 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.libraries.designsystem.component.async
|
||||
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.libraries.designsystem.components.async.AsyncIndicatorItem
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorState
|
||||
import io.element.android.libraries.designsystem.components.async.hasEntered
|
||||
import io.element.android.libraries.designsystem.components.async.hasExited
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AsyncIndicatorTests {
|
||||
@Test
|
||||
fun `initial state`() = runTest {
|
||||
val state = AsyncIndicatorState()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val transitionState = fakeAsyncIndicatorHost(state = state)
|
||||
val item = state.currentItem.value
|
||||
Snapshot(
|
||||
currentItem = item,
|
||||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add item with timeout`() = runTest(StandardTestDispatcher()) {
|
||||
val state = AsyncIndicatorState()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val transitionState = fakeAsyncIndicatorHost(state = state)
|
||||
val item = state.currentItem.value
|
||||
Snapshot(
|
||||
currentItem = item,
|
||||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
skipItems(1)
|
||||
state.enqueue(durationMs = 1000, composable = {})
|
||||
// Give it some time to pre-load the events
|
||||
advanceTimeBy(1000)
|
||||
runCurrent()
|
||||
// First, item is invisible but the target state is visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is not visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Then, item is not visible and the target state is not visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Finally, the current item is removed
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add item without timeout`() = runTest(StandardTestDispatcher()) {
|
||||
val state = AsyncIndicatorState()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val transitionState = fakeAsyncIndicatorHost(state = state)
|
||||
val item = state.currentItem.value
|
||||
Snapshot(
|
||||
currentItem = item,
|
||||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
skipItems(1)
|
||||
state.enqueue(composable = {})
|
||||
// First, item is invisible but the target state is visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// That's all, the current item will be displayed indefinitely
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add item without timeout then clear`() = runTest(StandardTestDispatcher()) {
|
||||
val state = AsyncIndicatorState()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val transitionState = fakeAsyncIndicatorHost(state = state)
|
||||
val item = state.currentItem.value
|
||||
Snapshot(
|
||||
currentItem = item,
|
||||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
skipItems(1)
|
||||
state.enqueue(composable = {})
|
||||
// First, item is invisible but the target state is visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Clear the current item
|
||||
state.clear()
|
||||
// Animating the exit animation
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Current item is no longer visible
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Finally, the current item is removed
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add item without timeout, then another one`() = runTest(StandardTestDispatcher()) {
|
||||
val state = AsyncIndicatorState()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val transitionState = fakeAsyncIndicatorHost(state = state)
|
||||
val item = state.currentItem.value
|
||||
Snapshot(
|
||||
currentItem = item,
|
||||
currentAnimationState = TransitionStateSnapshot(transitionState),
|
||||
)
|
||||
}.test {
|
||||
var firstItem: Any? = null
|
||||
skipItems(1)
|
||||
state.enqueue(composable = {})
|
||||
state.enqueue(composable = {})
|
||||
// First, item is invisible but the target state is visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
firstItem = currentItem
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Then, item is visible and the target state is not visible (will start animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Then, item is not visible and the target state is not visible (stopped animating)
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isFalse()
|
||||
}
|
||||
// Then a new item will be not visible and its target animation visible
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(firstItem).isNotEqualTo(currentItem)
|
||||
assertThat(currentAnimationState.currentState).isFalse()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// Finally, the second item is visible and not animating
|
||||
with(awaitItem()) {
|
||||
assertThat(currentItem).isNotNull()
|
||||
assertThat(firstItem).isNotEqualTo(currentItem)
|
||||
assertThat(currentAnimationState.currentState).isTrue()
|
||||
assertThat(currentAnimationState.targetState).isTrue()
|
||||
}
|
||||
// That's all, the current item will be displayed indefinitely
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fakeAsyncIndicatorHost(state: AsyncIndicatorState): Transition<Boolean>? {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val transition = state.currentItem.value?.let {
|
||||
// If there is an item, update its transition state to simulate an animation
|
||||
updateTransition(state.currentAnimationState, label = "")
|
||||
}
|
||||
if (state.currentAnimationState.hasEntered() && state.currentItem.value?.durationMs != null) {
|
||||
SideEffect {
|
||||
coroutineScope.launch {
|
||||
delay(state.currentItem.value!!.durationMs!!)
|
||||
state.nextState()
|
||||
}
|
||||
}
|
||||
} else if (state.currentItem.value != null && state.currentAnimationState.hasExited()) {
|
||||
SideEffect {
|
||||
state.nextState()
|
||||
}
|
||||
}
|
||||
return transition
|
||||
}
|
||||
|
||||
private data class Snapshot(
|
||||
val currentItem: AsyncIndicatorItem?,
|
||||
val currentAnimationState: TransitionStateSnapshot,
|
||||
)
|
||||
|
||||
private data class TransitionStateSnapshot(
|
||||
val currentState: Boolean,
|
||||
val targetState: Boolean,
|
||||
) {
|
||||
constructor(transition: Transition<Boolean>?) : this(
|
||||
currentState = transition?.currentState ?: false,
|
||||
targetState = transition?.targetState ?: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue