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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,9 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
|||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
|
||||
interface MatrixClient : Closeable {
|
||||
|
|
@ -43,6 +45,7 @@ interface MatrixClient : Closeable {
|
|||
val roomListService: RoomListService
|
||||
val mediaLoader: MatrixMediaLoader
|
||||
val sessionCoroutineScope: CoroutineScope
|
||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
suspend fun findDM(userId: UserId): RoomId?
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -62,16 +62,24 @@ import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
|||
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.util.SessionDirectoryNameProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
|
@ -79,6 +87,7 @@ import org.matrix.rustcomponents.sdk.BackupState
|
|||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
import org.matrix.rustcomponents.sdk.PowerLevels
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
|
|
@ -240,6 +249,16 @@ class RustMatrixClient(
|
|||
|
||||
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate)
|
||||
|
||||
override val ignoredUsersFlow = mxCallbackFlow<ImmutableList<UserId>> {
|
||||
client.subscribeToIgnoredUsers(object : IgnoredUsersListener {
|
||||
override fun call(ignoredUserIds: List<String>) {
|
||||
channel.trySend(ignoredUserIds.map(::UserId).toPersistentList())
|
||||
}
|
||||
})
|
||||
}
|
||||
.buffer(Channel.UNLIMITED)
|
||||
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
||||
|
||||
init {
|
||||
roomListService.state.onEach { state ->
|
||||
if (state == RoomListService.State.Running) {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,11 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
class FakeMatrixClient(
|
||||
|
|
@ -70,6 +73,8 @@ class FakeMatrixClient(
|
|||
var removeAvatarCalled: Boolean = false
|
||||
private set
|
||||
|
||||
override val ignoredUsersFlow: MutableStateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
|
||||
|
||||
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
|
|
|
|||
|
|
@ -236,9 +236,6 @@
|
|||
<string name="invite_friends_text">"Гэй, пагавары са мной у %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake паведаміць пра памылку"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Разблакіраваць"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Разблакіраваць карыстальніка"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
|
||||
|
|
|
|||
|
|
@ -194,8 +194,6 @@
|
|||
<string name="invite_friends_rich_title">"🔐️ Присъединете се към мен в %1$s"</string>
|
||||
<string name="invite_friends_text">"Хей, говорете с мен в %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Отблокиране"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Отблокиране на потребителя"</string>
|
||||
<string name="screen_share_location_title">"Споделяне на местоположение"</string>
|
||||
<string name="screen_share_my_location_action">"Споделяне на моето местоположение"</string>
|
||||
<string name="screen_share_open_apple_maps">"Отваряне в Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -237,9 +237,6 @@
|
|||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokovat"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Odblokovat uživatele"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
|
|||
|
|
@ -237,10 +237,6 @@
|
|||
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Schüttel heftig zum Melden von Fehlern"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Du kannst dann wieder alle Nachrichten von ihnen sehen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Blockierung aufheben"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Blockierung wird aufgehoben…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."</string>
|
||||
|
|
|
|||
|
|
@ -231,9 +231,6 @@
|
|||
<string name="invite_friends_text">"Hola, puedes hablar conmigo en %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Agitar con fuerza para informar de un error"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Desbloquear usuario"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Error al seleccionar archivos multimedia, por favor inténtalo de nuevo."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
|
||||
|
|
|
|||
|
|
@ -234,9 +234,6 @@
|
|||
<string name="invite_friends_text">"Salut, parle-moi sur %1$s : %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake pour signaler un problème"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Débloquer"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Débloquer l’utilisateur"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>
|
||||
|
|
|
|||
|
|
@ -233,9 +233,6 @@
|
|||
<string name="invite_friends_text">"Beszélgessünk a(z) %1$s: %2$s -n"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Az eszköz rázása a hibajelentéshez"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Letiltás feloldása"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Felhasználó kitiltásának feloldása"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nem sikerült kiválasztani a médiát, próbálja újra."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nem sikerült a média feltöltése, próbálja újra."</string>
|
||||
|
|
|
|||
|
|
@ -227,9 +227,6 @@
|
|||
<string name="invite_friends_text">"Hai, bicaralah dengan saya di %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake untuk melaporkan kutu"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Buka blokir"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Buka blokir pengguna"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Gagal memilih media, silakan coba lagi."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Gagal mengunggah media, silakan coba lagi."</string>
|
||||
|
|
|
|||
|
|
@ -233,9 +233,6 @@
|
|||
<string name="invite_friends_text">"Ehi, parlami su %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Scuoti per segnalare un problema"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Sblocca utente"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selezione del file multimediale fallita, riprova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Elaborazione del file multimediale da caricare fallita, riprova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Caricamento del file multimediale fallito, riprova."</string>
|
||||
|
|
|
|||
|
|
@ -238,9 +238,6 @@
|
|||
<string name="invite_friends_text">"Hei, vorbește cu mine pe %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake pentru a raporta erori"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selectarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Încărcarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@
|
|||
<string name="common_analytics">"Аналитика"</string>
|
||||
<string name="common_appearance">"Внешний вид"</string>
|
||||
<string name="common_audio">"Аудио"</string>
|
||||
<string name="common_blocked_users">"Заблокированные пользователи"</string>
|
||||
<string name="common_bubbles">"Пузыри"</string>
|
||||
<string name="common_chat_backup">"Резервная копия чатов"</string>
|
||||
<string name="common_copyright">"Авторское право"</string>
|
||||
|
|
@ -129,7 +130,9 @@
|
|||
<string name="common_enter_your_pin">"Введите свой PIN-код"</string>
|
||||
<string name="common_error">"Ошибка"</string>
|
||||
<string name="common_everyone">"Для всех"</string>
|
||||
<string name="common_failed">"Ошибка"</string>
|
||||
<string name="common_favourite">"Избранное"</string>
|
||||
<string name="common_favourited">"Избранное"</string>
|
||||
<string name="common_file">"Файл"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Файл сохранен в «Загрузки»"</string>
|
||||
<string name="common_forward_message">"Переслать сообщение"</string>
|
||||
|
|
@ -155,6 +158,7 @@
|
|||
<string name="common_mute">"Без звука"</string>
|
||||
<string name="common_no_results">"Ничего не найдено"</string>
|
||||
<string name="common_offline">"Не в сети"</string>
|
||||
<string name="common_or">"или"</string>
|
||||
<string name="common_password">"Пароль"</string>
|
||||
<string name="common_people">"Люди"</string>
|
||||
<string name="common_permalink">"Постоянная ссылка"</string>
|
||||
|
|
@ -237,9 +241,6 @@
|
|||
<string name="invite_friends_text">"Привет, поговори со мной по %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Встряхните устройство, чтобы сообщить об ошибке"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Разблокировать"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Разблокировать пользователя"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
|
||||
|
|
|
|||
|
|
@ -237,9 +237,6 @@
|
|||
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokovať"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Odblokovať používateľa"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>
|
||||
|
|
|
|||
|
|
@ -173,9 +173,6 @@
|
|||
<string name="invite_friends_text">"Hallå, prata med mig på %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Raseriskaka för att rapportera bugg"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Avblockera"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Avblockera användare"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Misslyckades att välja media, vänligen pröva igen."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Misslyckades att ladda upp media, vänligen pröva igen."</string>
|
||||
|
|
|
|||
|
|
@ -238,9 +238,6 @@
|
|||
<string name="invite_friends_text">"Привіт, пишіть мені за адресою %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Повідомити про ваду за допомогою Rageshake"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Розблокувати"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Розблокувати користувача"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не вдалося вибрати медіафайл, спробуйте ще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не вдалося завантажити медіафайл, спробуйте ще раз."</string>
|
||||
|
|
|
|||
|
|
@ -213,8 +213,6 @@
|
|||
<string name="error_some_messages_have_not_been_sent">"有些訊息尚未傳送"</string>
|
||||
<string name="invite_friends_text">"嘿,來 %1$s 和我聊天:%2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"解除封鎖"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"解除封鎖使用者"</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
|
||||
<string name="screen_share_location_title">"分享位置"</string>
|
||||
<string name="screen_share_my_location_action">"分享我的位置"</string>
|
||||
|
|
|
|||
|
|
@ -237,10 +237,6 @@
|
|||
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Unblock"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Unblock user"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Unblocking…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue