Browse Source
Changes recording button behaviour so that - tapping the record button starts a recording and displays the stop button - tapping the stop button stops the recording - tapping the delete button cancels the recording - 'hold to record' tooltip is removed --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>pull/1803/head
jonnyandrew
10 months ago
committed by
GitHub
37 changed files with 221 additions and 576 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Update voice message recording behaviour. Instead of holding the record button, users can now tap the record button to start recording and tap again to stop recording. |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
||||
android:height="24dp" |
||||
android:viewportWidth="24" |
||||
android:viewportHeight="24"> |
||||
<path |
||||
android:pathData="M6,16V8C6,7.45 6.196,6.979 6.588,6.588C6.979,6.196 7.45,6 8,6H16C16.55,6 17.021,6.196 17.413,6.588C17.804,6.979 18,7.45 18,8V16C18,16.55 17.804,17.021 17.413,17.413C17.021,17.804 16.55,18 16,18H8C7.45,18 6.979,17.804 6.588,17.413C6.196,17.021 6,16.55 6,16Z" |
||||
android:fillColor="#ffffff"/> |
||||
</vector> |
@ -1,189 +0,0 @@
@@ -1,189 +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. |
||||
*/ |
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class) |
||||
|
||||
package io.element.android.libraries.textcomposer.components |
||||
|
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.material3.TooltipState |
||||
import androidx.compose.material3.rememberTooltipState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType |
||||
import androidx.compose.ui.input.pointer.PointerEventType |
||||
import androidx.compose.ui.input.pointer.pointerInput |
||||
import androidx.compose.ui.platform.LocalHapticFeedback |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.designsystem.components.tooltip.ElementTooltipDefaults |
||||
import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip |
||||
import io.element.android.libraries.designsystem.components.tooltip.TooltipBox |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.IconButton |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables |
||||
import io.element.android.libraries.textcomposer.R |
||||
import io.element.android.libraries.textcomposer.utils.PressState |
||||
import io.element.android.libraries.textcomposer.utils.PressStateEffects |
||||
import io.element.android.libraries.textcomposer.utils.rememberPressState |
||||
import io.element.android.libraries.theme.ElementTheme |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
import kotlinx.coroutines.launch |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
internal fun RecordButton( |
||||
modifier: Modifier = Modifier, |
||||
initialTooltipIsVisible: Boolean = false, |
||||
onPressStart: () -> Unit = {}, |
||||
onLongPressEnd: () -> Unit = {}, |
||||
onTap: () -> Unit = {}, |
||||
) { |
||||
val coroutineScope = rememberCoroutineScope() |
||||
val pressState = rememberPressState() |
||||
val hapticFeedback = LocalHapticFeedback.current |
||||
|
||||
val performHapticFeedback = { |
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
} |
||||
|
||||
val tooltipState = rememberTooltipState( |
||||
initialIsVisible = initialTooltipIsVisible |
||||
) |
||||
|
||||
PressStateEffects( |
||||
pressState = pressState.value, |
||||
onPressStart = { |
||||
onPressStart() |
||||
performHapticFeedback() |
||||
}, |
||||
onLongPressEnd = { |
||||
onLongPressEnd() |
||||
performHapticFeedback() |
||||
}, |
||||
onTap = { |
||||
onTap() |
||||
performHapticFeedback() |
||||
coroutineScope.launch { tooltipState.show() } |
||||
}, |
||||
) |
||||
Box(modifier = modifier) { |
||||
HoldToRecordTooltip( |
||||
tooltipState = tooltipState, |
||||
spacingBetweenTooltipAndAnchor = 0.dp, // Accounts for the 48.dp size of the record button |
||||
anchor = { |
||||
RecordButtonView( |
||||
isPressed = pressState.value is PressState.Pressing, |
||||
modifier = Modifier |
||||
.pointerInput(Unit) { |
||||
awaitPointerEventScope { |
||||
while (true) { |
||||
val event = awaitPointerEvent() |
||||
coroutineScope.launch { |
||||
when (event.type) { |
||||
PointerEventType.Press -> pressState.press() |
||||
PointerEventType.Release -> pressState.release() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun RecordButtonView( |
||||
isPressed: Boolean, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
IconButton( |
||||
modifier = modifier |
||||
.size(48.dp), |
||||
onClick = {}, |
||||
) { |
||||
Icon( |
||||
modifier = Modifier.size(24.dp), |
||||
resourceId = if (isPressed) { |
||||
CommonDrawables.ic_compound_mic_on_solid |
||||
} else { |
||||
CommonDrawables.ic_compound_mic_on_outline |
||||
}, |
||||
contentDescription = stringResource(CommonStrings.a11y_voice_message_record), |
||||
tint = ElementTheme.colors.iconSecondary, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun HoldToRecordTooltip( |
||||
tooltipState: TooltipState, |
||||
spacingBetweenTooltipAndAnchor: Dp, |
||||
modifier: Modifier = Modifier, |
||||
anchor: @Composable () -> Unit, |
||||
) { |
||||
TooltipBox( |
||||
positionProvider = ElementTooltipDefaults.rememberPlainTooltipPositionProvider( |
||||
spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor, |
||||
), |
||||
tooltip = { |
||||
PlainTooltip { |
||||
Text( |
||||
text = stringResource(R.string.screen_room_voice_message_tooltip), |
||||
color = ElementTheme.colors.textOnSolidPrimary, |
||||
style = ElementTheme.typography.fontBodySmMedium, |
||||
) |
||||
} |
||||
}, |
||||
state = tooltipState, |
||||
modifier = modifier, |
||||
focusable = false, |
||||
enableUserInput = false, |
||||
content = anchor, |
||||
) |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun RecordButtonPreview() = ElementPreview { |
||||
Row { |
||||
RecordButtonView(isPressed = false) |
||||
RecordButtonView(isPressed = true) |
||||
} |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun HoldToRecordTooltipPreview() = ElementPreview { |
||||
Box(modifier = Modifier.fillMaxSize()) { |
||||
RecordButton( |
||||
modifier = Modifier.align(Alignment.BottomEnd), |
||||
initialTooltipIsVisible = true, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
/* |
||||
* 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.libraries.textcomposer.components |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType |
||||
import androidx.compose.ui.platform.LocalHapticFeedback |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.IconButton |
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables |
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent |
||||
import io.element.android.libraries.theme.ElementTheme |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
internal fun VoiceMessageRecorderButton( |
||||
isRecording: Boolean, |
||||
modifier: Modifier = Modifier, |
||||
onEvent: (VoiceMessageRecorderEvent) -> Unit = {}, |
||||
) { |
||||
val hapticFeedback = LocalHapticFeedback.current |
||||
|
||||
val performHapticFeedback = { |
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
} |
||||
|
||||
if (isRecording) { |
||||
StopButton( |
||||
modifier = modifier, |
||||
onClick = { |
||||
performHapticFeedback() |
||||
onEvent(VoiceMessageRecorderEvent.Stop) |
||||
} |
||||
) |
||||
} else { |
||||
StartButton( |
||||
modifier = modifier, |
||||
onClick = { |
||||
performHapticFeedback() |
||||
onEvent(VoiceMessageRecorderEvent.Start) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun StartButton( |
||||
onClick: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) = IconButton( |
||||
modifier = modifier.size(48.dp), |
||||
onClick = onClick, |
||||
) { |
||||
Icon( |
||||
modifier = Modifier.size(24.dp), |
||||
resourceId = CommonDrawables.ic_compound_mic_on_outline, |
||||
contentDescription = stringResource(CommonStrings.a11y_voice_message_record), |
||||
tint = ElementTheme.colors.iconSecondary, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun StopButton( |
||||
onClick: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) = IconButton( |
||||
modifier = modifier |
||||
.size(48.dp), |
||||
onClick = onClick, |
||||
) { |
||||
Box( |
||||
Modifier |
||||
.size(36.dp) |
||||
.background( |
||||
color = ElementTheme.colors.bgActionPrimaryRest, |
||||
shape = CircleShape, |
||||
) |
||||
) |
||||
Icon( |
||||
modifier = Modifier.size(24.dp), |
||||
resourceId = CommonDrawables.ic_stop, |
||||
contentDescription = stringResource(CommonStrings.a11y_voice_message_stop_recording), |
||||
tint = ElementTheme.colors.iconOnSolidPrimary, |
||||
) |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun VoiceMessageRecorderButtonPreview() = ElementPreview { |
||||
Row { |
||||
VoiceMessageRecorderButton(isRecording = false) |
||||
VoiceMessageRecorderButton(isRecording = true) |
||||
} |
||||
} |
@ -1,31 +0,0 @@
@@ -1,31 +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.libraries.textcomposer.utils |
||||
|
||||
/** |
||||
* State of a press gesture. |
||||
*/ |
||||
internal sealed interface PressState { |
||||
data class Idle( |
||||
val lastPress: Pressing? |
||||
) : PressState |
||||
|
||||
sealed interface Pressing : PressState |
||||
data object Tapping : Pressing |
||||
data object LongPressing : Pressing |
||||
} |
||||
|
@ -1,47 +0,0 @@
@@ -1,47 +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.libraries.textcomposer.utils |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
|
||||
/** |
||||
* React to [PressState] changes. |
||||
*/ |
||||
@Composable |
||||
internal fun PressStateEffects( |
||||
pressState: PressState, |
||||
onPressStart: () -> Unit = {}, |
||||
onLongPressStart: () -> Unit = {}, |
||||
onTap: () -> Unit = {}, |
||||
onLongPressEnd: () -> Unit = {}, |
||||
) { |
||||
LaunchedEffect(pressState) { |
||||
when (pressState) { |
||||
is PressState.Idle -> |
||||
when (pressState.lastPress) { |
||||
PressState.Tapping -> onTap() |
||||
PressState.LongPressing -> onLongPressEnd() |
||||
null -> {} // Do nothing |
||||
} |
||||
is PressState.LongPressing -> onLongPressStart() |
||||
PressState.Tapping -> onPressStart() |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
@ -1,101 +0,0 @@
@@ -1,101 +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.libraries.textcomposer.utils |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.State |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.compose.ui.platform.LocalViewConfiguration |
||||
import kotlinx.coroutines.Job |
||||
import kotlinx.coroutines.coroutineScope |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.isActive |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.yield |
||||
import timber.log.Timber |
||||
|
||||
@Composable |
||||
internal fun rememberPressState( |
||||
longPressTimeoutMillis: Long = LocalViewConfiguration.current.longPressTimeoutMillis, |
||||
): PressStateHolder { |
||||
return remember(longPressTimeoutMillis) { |
||||
PressStateHolder(longPressTimeoutMillis = longPressTimeoutMillis) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* State machine that keeps track of the pressed state. |
||||
* |
||||
* When a press is started, the state will transition through: |
||||
* [PressState.Idle] -> [PressState.Tapping] -> ... |
||||
* |
||||
* If a press is held for a longer time, the state will continue through: |
||||
* ... -> [PressState.LongPressing] -> ... |
||||
* |
||||
* When the press is released the states will then transition back to idle. |
||||
* ... -> [PressState.Idle] |
||||
* |
||||
* Whether a press should be considered a tap or a long press can be determined by |
||||
* looking at the last press when in the idle state. |
||||
* |
||||
* @see [PressStateEffects] |
||||
* @see [rememberPressState] |
||||
*/ |
||||
internal class PressStateHolder( |
||||
private val longPressTimeoutMillis: Long, |
||||
) : State<PressState> { |
||||
private var state: PressState by mutableStateOf(PressState.Idle(lastPress = null)) |
||||
|
||||
override val value: PressState |
||||
get() = state |
||||
|
||||
private var longPressTimer: Job? = null |
||||
|
||||
suspend fun press() = coroutineScope { |
||||
when (state) { |
||||
is PressState.Idle -> { |
||||
state = PressState.Tapping |
||||
} |
||||
is PressState.Pressing -> |
||||
Timber.e("Pointer pressed but it has not been released") |
||||
} |
||||
|
||||
longPressTimer = launch { |
||||
delay(longPressTimeoutMillis) |
||||
yield() |
||||
|
||||
if (isActive && state == PressState.Tapping) { |
||||
state = PressState.LongPressing |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun release() { |
||||
longPressTimer?.cancel() |
||||
longPressTimer = null |
||||
when (val lastState = state) { |
||||
is PressState.Pressing -> |
||||
state = PressState.Idle(lastPress = lastState) |
||||
is PressState.Idle -> |
||||
Timber.e("Pointer pressed but it has not been released") |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,111 +0,0 @@
@@ -1,111 +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.libraries.textcomposer.utils |
||||
|
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.libraries.textcomposer.utils.PressState.Idle |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.async |
||||
import kotlinx.coroutines.test.advanceTimeBy |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
import kotlin.time.Duration.Companion.milliseconds |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class PressStateHolderTest { |
||||
companion object { |
||||
const val LONG_PRESS_TIMEOUT_MILLIS = 1L |
||||
} |
||||
@Test |
||||
fun `it starts in idle state`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = null)) |
||||
} |
||||
|
||||
@Test |
||||
fun `when press, it moves to tapping state`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
val press = async { stateHolder.press() } |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||
press.await() |
||||
} |
||||
|
||||
@Test |
||||
fun `when release after short delay, it moves through tap states`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
val press = async { stateHolder.press() } |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||
stateHolder.release() |
||||
advanceTimeBy(1.milliseconds) // wait for the long press timeout which should not be triggered |
||||
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.Tapping)) |
||||
press.await() |
||||
} |
||||
|
||||
@Test |
||||
fun `when hold, it moves through long press states`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
val press = async { stateHolder.press() } |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.LongPressing) |
||||
stateHolder.release() |
||||
assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.LongPressing)) |
||||
press.await() |
||||
} |
||||
|
||||
@Test |
||||
fun `when release and repress, it doesn't enter long press states`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
val press1 = async { stateHolder.press() } |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||
stateHolder.release() |
||||
val press2 = async { stateHolder.press() } |
||||
advanceTimeBy(1.milliseconds) |
||||
assertThat(stateHolder.value).isEqualTo(PressState.Tapping) |
||||
press1.await() |
||||
press2.await() |
||||
} |
||||
|
||||
@Test |
||||
fun `when press twice without releasing, it doesn't throw an error`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
stateHolder.press() |
||||
stateHolder.press() |
||||
} |
||||
|
||||
@Test |
||||
fun `when release without first pressing, it doesn't throw an error`() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
stateHolder.release() |
||||
} |
||||
|
||||
@Test |
||||
fun `when release twice without pressing, it doesn't throw an error `() = runTest { |
||||
val stateHolder = createStateHolder() |
||||
stateHolder.press() |
||||
stateHolder.release() |
||||
stateHolder.release() |
||||
} |
||||
|
||||
private fun createStateHolder() = |
||||
PressStateHolder( |
||||
LONG_PRESS_TIMEOUT_MILLIS, |
||||
) |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-16_16_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-14_14_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-16_16_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-14_14_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-16_17_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-14_15_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-16_17_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-14_15_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-17_17_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-15_15_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-17_17_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-15_15_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-17_18_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-15_16_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-17_18_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-15_16_null,NEXUS_5,1.0,en].png
Binary file not shown.
Binary file not shown.
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-18_18_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-17_17_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-18_18_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-17_17_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-18_19_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-17_18_null,NEXUS_5,1.0,en].png
0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-18_19_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-17_18_null,NEXUS_5,1.0,en].png
Binary file not shown.
Loading…
Reference in new issue