diff --git a/.maestro/tests/settings/settings.yaml b/.maestro/tests/settings/settings.yaml index 53fcf0f017..d5f1e110e5 100644 --- a/.maestro/tests/settings/settings.yaml +++ b/.maestro/tests/settings/settings.yaml @@ -16,7 +16,7 @@ appId: ${APP_ID} - tapOn: text: "Report a problem" -- assertVisible: "Report a bug" +- assertVisible: "Report a problem" - back - tapOn: diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index c1bed67d80..03f0b20429 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -18,6 +18,7 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer +import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent @@ -34,6 +35,7 @@ class ElementXApplication : Application(), DaggerComponentOwner { AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) initializeComponent(TracingInitializer::class.java) + initializeComponent(CacheCleanerInitializer::class.java) } logApplicationInfo() } diff --git a/changelog.d/1824.misc b/changelog.d/1824.misc new file mode 100644 index 0000000000..16cdc522ed --- /dev/null +++ b/changelog.d/1824.misc @@ -0,0 +1 @@ +Suppress usage of removeTimeline method. diff --git a/features/cachecleaner/api/build.gradle.kts b/features/cachecleaner/api/build.gradle.kts new file mode 100644 index 0000000000..38788af301 --- /dev/null +++ b/features/cachecleaner/api/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.features.cachecleaner.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(libs.androidx.startup) +} diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt new file mode 100644 index 0000000000..cd26d87bf9 --- /dev/null +++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.cachecleaner.api + +interface CacheCleaner { + /** + * Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages). + * + * Will fail silently in case of errors while deleting the files. + */ + fun clearCache() +} diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt new file mode 100644 index 0000000000..6492f9e62d --- /dev/null +++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.cachecleaner.api + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +interface CacheCleanerBindings { + fun cacheCleaner(): CacheCleaner +} diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt new file mode 100644 index 0000000000..5cd17c8715 --- /dev/null +++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.cachecleaner.api + +import android.content.Context +import androidx.startup.Initializer +import io.element.android.libraries.architecture.bindings + +class CacheCleanerInitializer : Initializer { + override fun create(context: Context) { + context.bindings().cacheCleaner().clearCache() + } + + override fun dependencies(): List>> = emptyList() +} diff --git a/features/cachecleaner/impl/build.gradle.kts b/features/cachecleaner/impl/build.gradle.kts new file mode 100644 index 0000000000..c95619419b --- /dev/null +++ b/features/cachecleaner/impl/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.features.cachecleaner.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.cachecleaner.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.truth) + testImplementation(projects.tests.testutils) +} diff --git a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt new file mode 100644 index 0000000000..fc77a5934a --- /dev/null +++ b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.cachecleaner.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.cachecleaner.api.CacheCleaner +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.CacheDirectory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +/** + * Default implementation of [CacheCleaner]. + */ +@ContributesBinding(AppScope::class) +class DefaultCacheCleaner @Inject constructor( + private val scope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, + @CacheDirectory private val cacheDir: File, +) : CacheCleaner { + companion object { + val SUBDIRS_TO_CLEANUP = listOf("temp/media", "temp/voice") + } + + override fun clearCache() { + scope.launch(dispatchers.io) { + runCatching { + SUBDIRS_TO_CLEANUP.forEach { + File(cacheDir.path, it).apply { + if (exists()) { + if (!deleteRecursively()) error("Failed to delete recursively cache directory $this") + } + if (!mkdirs()) error("Failed to create cache directory $this") + } + } + }.onFailure { + Timber.e(it, "Failed to clear cache") + } + } + } +} diff --git a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt new file mode 100644 index 0000000000..7fe70028ac --- /dev/null +++ b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.cachecleaner.impl + +import com.google.common.truth.Truth +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class DefaultCacheCleanerTest { + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `calling clearCache actually removes file in the SUBDIRS_TO_CLEANUP list`() = runTest { + // Create temp subdirs and fill with 2 files each + DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach { + File(temporaryFolder.root, it).apply { + mkdirs() + File(this, "temp1").createNewFile() + File(this, "temp2").createNewFile() + } + } + + // Clear cache + aCacheCleaner().clearCache() + + // Check the files are gone but the sub dirs are not. + DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach { + File(temporaryFolder.root, it).apply { + Truth.assertThat(exists()).isTrue() + Truth.assertThat(isDirectory).isTrue() + Truth.assertThat(listFiles()).isEmpty() + } + } + } + + @Test + fun `clear cache fails silently`() = runTest { + // Set cache dir as unreadable, unwritable and unexecutable so that the deletion fails. + check(temporaryFolder.root.setReadable(false)) + check(temporaryFolder.root.setWritable(false)) + check(temporaryFolder.root.setExecutable(false)) + + aCacheCleaner().clearCache() + } + + private fun TestScope.aCacheCleaner() = DefaultCacheCleaner( + scope = this, + dispatchers = this.testCoroutineDispatchers(true), + cacheDir = temporaryFolder.root, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 1361415dca..0bad8f8830 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -32,6 +32,7 @@ import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState +import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -67,6 +68,7 @@ class TimelinePresenter @Inject constructor( private val verificationService: SessionVerificationService, private val encryptionService: EncryptionService, private val featureFlagService: FeatureFlagService, + private val redactedVoiceMessageManager: RedactedVoiceMessageManager, ) : Presenter { private val timeline = room.timeline @@ -159,6 +161,7 @@ class TimelinePresenter @Inject constructor( paginateBackwards() } } + .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) .launchIn(this) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 839501e429..5f7fff64ae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -421,22 +421,6 @@ private fun MessageEventBubbleContent( // to its `combinedClickable` parent so we do it manually fun onTimestampLongClick() = onMessageLongClick() - @Composable - fun ContentView( - modifier: Modifier = Modifier - ) { - TimelineItemEventContentView( - content = event.content, - isMine = event.isMine, - interactionSource = interactionSource, - onClick = onMessageClick, - onLongClick = onMessageLongClick, - extraPadding = event.toExtraPadding(), - eventSink = eventSink, - modifier = modifier, - ) - } - @Composable fun ThreadDecoration( modifier: Modifier = Modifier @@ -460,21 +444,20 @@ private fun MessageEventBubbleContent( } @Composable - fun ContentAndTimestampView( + fun WithTimestampLayout( timestampPosition: TimestampPosition, modifier: Modifier = Modifier, - contentModifier: Modifier = Modifier, - timestampModifier: Modifier = Modifier, + content: @Composable () -> Unit, ) { when (timestampPosition) { TimestampPosition.Overlay -> Box(modifier) { - ContentView(modifier = contentModifier) + content() TimelineEventTimestampView( event = event, onClick = onTimestampClicked, onLongClick = ::onTimestampLongClick, - modifier = timestampModifier + modifier = Modifier .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding .background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp)) .align(Alignment.BottomEnd) @@ -483,24 +466,24 @@ private fun MessageEventBubbleContent( } TimestampPosition.Aligned -> Box(modifier) { - ContentView(modifier = contentModifier) + content() TimelineEventTimestampView( event = event, onClick = onTimestampClicked, onLongClick = ::onTimestampLongClick, - modifier = timestampModifier + modifier = Modifier .align(Alignment.BottomEnd) .padding(horizontal = 8.dp, vertical = 4.dp) ) } TimestampPosition.Below -> Column(modifier) { - ContentView(modifier = contentModifier) + content() TimelineEventTimestampView( event = event, onClick = onTimestampClicked, onLongClick = ::onTimestampLongClick, - modifier = timestampModifier + modifier = Modifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -516,52 +499,77 @@ private fun MessageEventBubbleContent( inReplyToDetails: InReplyTo.Ready?, modifier: Modifier = Modifier ) { - val modifierWithPadding: Modifier + val timestampLayoutModifier: Modifier val contentModifier: Modifier when { inReplyToDetails != null -> { if (timestampPosition == TimestampPosition.Overlay) { - modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + timestampLayoutModifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) } else { contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) - modifierWithPadding = Modifier + timestampLayoutModifier = Modifier } } timestampPosition != TimestampPosition.Overlay -> { - modifierWithPadding = Modifier + timestampLayoutModifier = Modifier contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) } else -> { - modifierWithPadding = Modifier + timestampLayoutModifier = Modifier contentModifier = Modifier } } - - EqualWidthColumn(modifier = modifier, spacing = 8.dp) { + val threadDecoration = @Composable { if (showThreadDecoration) { ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp)) } - if (inReplyToDetails != null) { - val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) - val text = textForInReplyTo(inReplyToDetails) - val topPadding = if (showThreadDecoration) 0.dp else 8.dp - ReplyToContent( - senderName = senderName, - text = text, - attachmentThumbnailInfo = attachmentThumbnailInfo, - modifier = Modifier - .padding(top = topPadding, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = true, onClick = inReplyToClick), + } + val contentWithTimestamp = @Composable { + WithTimestampLayout( + timestampPosition = timestampPosition, + modifier = timestampLayoutModifier, + ) { + TimelineItemEventContentView( + content = event.content, + isMine = event.isMine, + interactionSource = interactionSource, + onClick = onMessageClick, + onLongClick = onMessageLongClick, + extraPadding = event.toExtraPadding(), + eventSink = eventSink, + modifier = contentModifier, ) } - ContentAndTimestampView( - timestampPosition = timestampPosition, - modifier = modifierWithPadding, - contentModifier = contentModifier, + } + val inReplyTo = @Composable { inReplyToReady: InReplyTo.Ready -> + val senderName = inReplyToReady.senderDisplayName ?: inReplyToReady.senderId.value + val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToReady) + val text = textForInReplyTo(inReplyToReady) + val topPadding = if (showThreadDecoration) 0.dp else 8.dp + ReplyToContent( + senderName = senderName, + text = text, + attachmentThumbnailInfo = attachmentThumbnailInfo, + modifier = Modifier + .padding(top = topPadding, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + .clickable(enabled = true, onClick = inReplyToClick), ) + + } + if (inReplyToDetails != null) { + // Use SubComposeLayout only if necessary as it can have consequences on the performance. + EqualWidthColumn(modifier = modifier, spacing = 8.dp) { + threadDecoration() + inReplyTo(inReplyToDetails) + contentWithTimestamp() + } + } else { + Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) { + threadDecoration() + contentWithTimestamp() + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt new file mode 100644 index 0000000000..fc9e98f0ed --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.voicemessages.timeline + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.mediaplayer.api.MediaPlayer +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface RedactedVoiceMessageManager { + suspend fun onEachMatrixTimelineItem(timelineItems: List) +} + +@ContributesBinding(RoomScope::class) +class DefaultRedactedVoiceMessageManager @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val mediaPlayer: MediaPlayer, +) : RedactedVoiceMessageManager { + override suspend fun onEachMatrixTimelineItem(timelineItems: List) { + withContext(dispatchers.computation) { + mediaPlayer.state.value.let { playerState -> + if (playerState.isPlaying && playerState.mediaId != null) { + val needsToPausePlayer = timelineItems.any { + it is MatrixTimelineItem.Event && + playerState.mediaId == it.eventId?.value && + it.event.content is RedactedContent + } + if (needsToPausePlayer) { + withContext(dispatchers.main) { mediaPlayer.pause() } + } + } + } + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt index a36eccc1d2..cc8bd1945f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt @@ -89,7 +89,6 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor( source = mediaSource, mimeType = mimeType, body = body, - useCache = false, ).mapCatching { it.use { mediaFile -> val dest = cachedFile.apply { parentFile?.mkdirs() } diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index a1ce610568..77f7e1d0e5 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -45,6 +45,7 @@ "Произошла ошибка при загрузке настроек уведомлений." "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." + "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате." "Все сообщения" "В этой комнате уведомить меня о" "Показать меньше" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index fd7efc1200..0bdf008fc2 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -45,6 +45,7 @@ "Pri načítavaní nastavení oznámení došlo k chybe." "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." + "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie." "Všetky správy" "V tejto miestnosti ma upozorniť na" "Zobraziť menej" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 5cb4609f1c..7fdb2885c4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -48,6 +48,7 @@ import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter +import io.element.android.features.messages.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.Async @@ -655,6 +656,7 @@ class MessagesPresenterTest { encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), + redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ) val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 9b8580a0ce..15ec930d2e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -30,6 +30,9 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager +import io.element.android.features.messages.voicemessages.timeline.FakeRedactedVoiceMessageManager +import io.element.android.features.messages.voicemessages.timeline.aRedactedMatrixTimeline import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem @@ -311,9 +314,31 @@ class TimelinePresenterTest { } } + @Test + fun `present - side effect on redacted items is invoked`() = runTest { + val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager() + val presenter = createTimelinePresenter( + timeline = FakeMatrixTimeline( + initialTimelineItems = aRedactedMatrixTimeline(AN_EVENT_ID), + ), + redactedVoiceMessageManager = redactedVoiceMessageManager, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) // skip initial state + assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0) + awaitItem().let { + assertThat(it.timelineItems).isNotEmpty() + assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1) + } + } + } + private fun TestScope.createTimelinePresenter( timeline: MatrixTimeline = FakeMatrixTimeline(), - timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory() + timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), + redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, @@ -324,6 +349,7 @@ class TimelinePresenterTest { encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), + redactedVoiceMessageManager = redactedVoiceMessageManager, ) } @@ -340,6 +366,7 @@ class TimelinePresenterTest { encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), + redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt new file mode 100644 index 0000000000..0ff58d7728 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.voicemessages.timeline + +import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem + +class FakeRedactedVoiceMessageManager : RedactedVoiceMessageManager { + + private val _invocations: MutableList> = mutableListOf() + val invocations: List> + get() = _invocations + + override suspend fun onEachMatrixTimelineItem(timelineItems: List) { + _invocations.add(timelineItems) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt new file mode 100644 index 0000000000..b8be4d4d50 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.voicemessages.timeline + +import com.google.common.truth.Truth +import io.element.android.features.messages.impl.voicemessages.timeline.DefaultRedactedVoiceMessageManager +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.mediaplayer.api.MediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RedactedVoiceMessageManagerTest { + @Test + fun `redacted event - no playing related media`() = runTest { + val mediaPlayer = FakeMediaPlayer().apply { + setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") + play() + } + val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) + + Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) + Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() + + manager.onEachMatrixTimelineItem(aRedactedMatrixTimeline(AN_EVENT_ID_2)) + + Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) + Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() + } + + @Test + fun `redacted event - playing related media is paused`() = runTest { + val mediaPlayer = FakeMediaPlayer().apply { + setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") + play() + } + val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) + + Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) + Truth.assertThat(mediaPlayer.state.value.isPlaying).isTrue() + + manager.onEachMatrixTimelineItem(aRedactedMatrixTimeline(AN_EVENT_ID)) + + Truth.assertThat(mediaPlayer.state.value.mediaId).isEqualTo(AN_EVENT_ID.value) + Truth.assertThat(mediaPlayer.state.value.isPlaying).isFalse() + } +} + +fun TestScope.aDefaultRedactedVoiceMessageManager( + mediaPlayer: MediaPlayer = FakeMediaPlayer(), +) = DefaultRedactedVoiceMessageManager( + dispatchers = this.testCoroutineDispatchers(true), + mediaPlayer = mediaPlayer, +) + +fun aRedactedMatrixTimeline(eventId: EventId) = listOf( + MatrixTimelineItem.Event( + uniqueId = 0, + event = EventTimelineItem( + eventId = eventId, + transactionId = null, + isEditable = false, + isLocal = false, + isOwn = false, + isRemote = false, + localSendState = null, + reactions = listOf(), + sender = A_USER_ID, + senderProfile = ProfileTimelineDetails.Unavailable, + timestamp = 9442, + content = RedactedContent, + debugInfo = TimelineItemDebugInfo( + model = "enim", + originalJson = null, + latestEditedJson = null + ), + origin = null + ), + ) +) diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index 9afb388a9a..3432902dea 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -28,6 +28,7 @@ "Включить уведомления на данном устройстве" "Конфигурация не была исправлена, попробуйте еще раз." "Групповые чаты" + "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления." "Упоминания" "Все" "Упоминания" diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index b1de800205..2b8f866255 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -30,6 +30,7 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Povoliť oznámenia na tomto zariadení" "Konfigurácia nebola opravená, skúste to prosím znova." "Skupinové rozhovory" + "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie." "Zmienky" "Všetky" "Zmienky" diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index 2cbb54cdad..7833c21764 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -64,7 +64,7 @@ fun BugReportView( Box(modifier = modifier) { PreferencePage( - title = stringResource(id = CommonStrings.common_report_a_bug), + title = stringResource(id = CommonStrings.common_report_a_problem), onBackPressed = onBackPressed ) { val isFormEnabled = state.sending !is Async.Loading diff --git a/features/rageshake/impl/src/main/res/values-ru/translations.xml b/features/rageshake/impl/src/main/res/values-ru/translations.xml index 8f05a3148d..c22a34bf30 100644 --- a/features/rageshake/impl/src/main/res/values-ru/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml @@ -4,8 +4,8 @@ "Вы можете связаться со мной, если у Вас возникнут какие-либо дополнительные вопросы." "Связаться со мной" "Редактировать снимок экрана" - "Пожалуйста, опишите ошибку. Что вы сделали? Что вы ожидали, что произойдет? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее." - "Опишите ошибку…" + "Пожалуйста, опишите ошибку. Что вы сделали? Какое поведение вы ожидали? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее." + "Опишите проблему…" "Если возможно, пожалуйста, напишите описание на английском языке." "Отправка журналов сбоев" "Разрешить ведение журналов" diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index fb02c93780..34ba8b5b30 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -4,8 +4,8 @@ "You may contact me if you have any follow up questions." "Contact me" "Edit screenshot" - "Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can." - "Describe the bug…" + "Please describe the problem. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can." + "Describe the problem…" "If possible, please write the description in English." "Send crash logs" "Allow logs" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 38a8835251..7d3eae6fff 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -36,6 +36,7 @@ "Произошла ошибка при загрузке настроек уведомлений." "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." + "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате." "Все сообщения" "В этой комнате уведомить меня о" "Заблокировать" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index edaf50d915..c7e422b920 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -36,6 +36,7 @@ "Pri načítavaní nastavení oznámení došlo k chybe." "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." + "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie." "Všetky správy" "V tejto miestnosti ma upozorniť na" "Zablokovať" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc2f1c74c7..7b71335553 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ [versions] # Project -android_gradle_plugin = "8.1.3" +android_gradle_plugin = "8.1.4" kotlin = "1.9.20" ksp = "1.9.20-1.0.14" firebaseAppDistribution = "4.0.1" @@ -13,7 +13,7 @@ core = "1.12.0" datastore = "1.0.0" constraintlayout = "2.1.4" constraintlayout_compose = "1.0.1" -lifecycle = "2.7.0-beta01" +lifecycle = "2.7.0-rc01" activity = "1.8.1" media3 = "1.1.1" @@ -33,11 +33,11 @@ test_core = "1.5.0" #other coil = "2.5.0" datetime = "0.4.1" -serialization_json = "1.6.0" +serialization_json = "1.6.1" showkase = "1.0.2" appyx = "1.4.0" sqldelight = "2.0.0" -wysiwyg = "2.16.0" +wysiwyg = "2.17.0" # DI dagger = "2.48.1" @@ -59,7 +59,7 @@ android_desugar = "com.android.tools:desugar_jdk_libs:2.0.4" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gms_google_services = "com.google.gms:google-services:4.4.0" # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.5.0" +google_firebase_bom = "com.google.firebase:firebase-bom:32.6.0" firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } # AndroidX @@ -155,7 +155,7 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" -telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.6.2" +telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.7.1" statemachine = "com.freeletics.flowredux:compose:1.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2" diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index d539ec6a53..2bfef368a9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -21,12 +21,11 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.ForwardEventException import io.element.android.libraries.matrix.impl.roomlist.roomOrNull +import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListService -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineListener import kotlin.time.Duration.Companion.milliseconds /** @@ -56,16 +55,14 @@ class RoomContentForwarder( val failedForwardingTo = mutableSetOf() targetRooms.parallelMap { room -> room.use { targetRoom -> - val result = runCatching { + runCatching { // Sending a message requires a registered timeline listener - targetRoom.addTimelineListener(NoOpTimelineListener) - withTimeout(timeoutMs.milliseconds) { - targetRoom.send(content) + targetRoom.runWithTimelineListenerRegistered { + withTimeout(timeoutMs.milliseconds) { + targetRoom.send(content) + } } } - // After sending, we remove the timeline - targetRoom.removeTimeline() - result }.onFailure { failedForwardingTo.add(RoomId(room.id())) if (it is CancellationException) { @@ -75,11 +72,7 @@ class RoomContentForwarder( } if (failedForwardingTo.isNotEmpty()) { - throw ForwardEventException(toRoomIds.toList()) + throw ForwardEventException(failedForwardingTo.toList()) } } - - private object NoOpTimelineListener : TimelineListener { - override fun onUpdate(diff: List) = Unit - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index bddd2bc872..e87ae74f30 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -70,3 +70,17 @@ internal fun Room.backPaginationStatusFlow(): Flow = subscribeToBackPaginationStatus(listener) } }.buffer(Channel.UNLIMITED) + +internal suspend fun Room.runWithTimelineListenerRegistered(action: suspend () -> Unit) { + val result = addTimelineListener(NoOpTimelineListener) + try { + action() + } finally { + result.itemsStream.cancelAndDestroy() + result.items.destroyAll() + } +} + +private object NoOpTimelineListener : TimelineListener { + override fun onUpdate(diff: List) = Unit +} diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 309e463faf..0b4700235b 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -9,12 +9,14 @@ "Воспроизвести" "Опрос" "Опрос завершен" + "Прочитано %1$s" "Отправить файлы" "Показать пароль" "Начать звонок" "Меню пользователя" "Записать голосовое сообщение." "Остановить запись" + "Прочитано %1$s и %2$s" "Разрешить" "Добавить в хронологию" "Назад" @@ -81,6 +83,7 @@ "Начать подтверждение" "Нажмите, чтобы загрузить карту" "Сделать фото" + "Нажмите для просмотра вариантов" "Повторить попытку" "Показать источник" "Да" @@ -89,12 +92,14 @@ "Политика допустимого использования" "Дополнительные параметры" "Аналитика" + "Оформление" "Аудио" "Пузыри" "Резервная копия чатов" "Авторское право" "Создание комнаты…" "Покинул комнату" + "Темная" "Ошибка расшифровки" "Для разработчика" "(изменено)" @@ -113,6 +118,7 @@ "Установить APK" "Идентификатор Matrix ID не найден, приглашение может быть не получено." "Покинуть комнату" + "Светлая" "Ссылка скопирована в буфер обмена" "Загрузка…" "Сообщение" @@ -146,7 +152,10 @@ "Поиск человека" "Результаты поиска" "Безопасность" + "Просмотрено" "Отправка…" + "Сбой отправки" + "Отправлено" "Сервер не поддерживается" "Адрес сервера" "Настройки" @@ -157,6 +166,7 @@ "Успешно" "Рекомендации" "Синхронизация" + "Системная" "Текст" "Уведомление о третьей стороне" "Обсуждение" @@ -198,6 +208,11 @@ "Ведено %1$d цифр" "Введено много цифр" + + "Прочитано %1$s и %2$d другим" + "Прочитано %1$s и %2$d другими" + "Прочитано %1$s и %2$d другими" + "%1$d участник" "%1$d участников" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 0ccbab9cb8..7fe0ffd9d1 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -9,12 +9,14 @@ "Prehrať" "Anketa" "Ukončená anketa" + "Prečítal/a %1$s" "Odoslať súbory" "Zobraziť heslo" "Začať hovor" "Používateľské menu" "Nahrať hlasovú správu." "Zastaviť nahrávanie" + "Prečítal/a %1$s a %2$s" "Prijať" "Pridať na časovú os" "Späť" @@ -81,6 +83,7 @@ "Spustiť overovanie" "Ťuknutím načítate mapu" "Urobiť fotku" + "Klepnutím získate možnosti" "Skúste to znova" "Zobraziť zdroj" "Áno" @@ -89,12 +92,14 @@ "Zásady prijateľného používania" "Pokročilé nastavenia" "Analytika" + "Vzhľad" "Zvuk" "Bubliny" "Záloha konverzácie" "Autorské práva" "Vytváranie miestnosti…" "Opustil/a miestnosť" + "Tmavý" "Chyba dešifrovania" "Možnosti pre vývojárov" "(upravené)" @@ -113,6 +118,7 @@ "Inštalovať APK" "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" + "Svetlý" "Odkaz bol skopírovaný do schránky" "Načítava sa…" "Správa" @@ -146,7 +152,10 @@ "Vyhľadať niekoho" "Výsledky hľadania" "Bezpečnosť" + "Videné" "Odosiela sa…" + "Odoslanie zlyhalo" + "Odoslané" "Server nie je podporovaný" "URL adresa servera" "Nastavenia" @@ -157,6 +166,7 @@ "Úspech" "Návrhy" "Synchronizuje sa" + "Systém" "Text" "Oznámenia tretích strán" "Vlákno" @@ -198,6 +208,11 @@ "%1$d zadané číslice" "%1$d zadaných číslic" + + "Prečítal/a %1$s a %2$d ďalší" + "Prečítal/a %1$s a %2$d ďalší" + "Prečítal/a %1$s a %2$d ďalších" + "%1$d člen" "%1$d členovia" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_0,NEXUS_5,1.0,en].png index 5f5be1fdc5..b7bb6bcf4c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf178a51be45cfd5fee676628ae3192374c1bc873e6eef726053ae593974aafe -size 68108 +oid sha256:3928ea1a60bf2b5c1cc94ef566ca67ce03679994430f826f50636c781976e8e2 +size 70068 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_1,NEXUS_5,1.0,en].png index 15e4e18e8e..7a638cc9f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:922ffb470cc64a29cd2d9a65840d267cb742efa523f3d93676ec148f818850e1 -size 204829 +oid sha256:62ddb0f9d6764b639b1a1438535c19138d1f81afdaaebfe03b021fc7f64a5291 +size 206670 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_2,NEXUS_5,1.0,en].png index aa132627e5..10d0f335e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:332e7a7baa12fb54fed4ad99f53470a96182e6059f3f24631ef9c5a7ed9c9f54 -size 59420 +oid sha256:6d546a6a3a5e517e99758f05396a72cc7bf802f3e207f5c7d881e5805f103b67 +size 61356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_3,NEXUS_5,1.0,en].png index 5f5be1fdc5..b7bb6bcf4c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Day-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf178a51be45cfd5fee676628ae3192374c1bc873e6eef726053ae593974aafe -size 68108 +oid sha256:3928ea1a60bf2b5c1cc94ef566ca67ce03679994430f826f50636c781976e8e2 +size 70068 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_0,NEXUS_5,1.0,en].png index adc80daf5f..d8461b8165 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:310bd5d524d61cbc3b8237811c8d6623a6fb1046bb8a9823ff5173c2f2b3c862 -size 65218 +oid sha256:53cb4046ffb2391e880bb9f4567534483f9c3e8bdbea7d7706d3fb93ec0acdec +size 67105 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_1,NEXUS_5,1.0,en].png index 618c296fbf..8e8c828451 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bd787f27931a5f71da9ed4248ff7c310acce37168b36e2575995ea1a1dc2cac -size 200484 +oid sha256:22c01def033c5ce1e7bc12dc7b5ec6fae11bb827f29818d3921f156a4cf04386 +size 202460 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_2,NEXUS_5,1.0,en].png index cda48a2eff..29f1413708 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e93d40807105a64a933180a71a73c661c4b792f5a008af7ada5fc8fd80a1446b -size 54960 +oid sha256:8960e097e94be82ab519bbc1ed3679955dd200349ae32e55d11fad1cbaf586ae +size 56261 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_3,NEXUS_5,1.0,en].png index adc80daf5f..d8461b8165 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.impl.bugreport_BugReportView_null_BugReportView-Night-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:310bd5d524d61cbc3b8237811c8d6623a6fb1046bb8a9823ff5173c2f2b3c862 -size 65218 +oid sha256:53cb4046ffb2391e880bb9f4567534483f9c3e8bdbea7d7706d3fb93ec0acdec +size 67105