Browse Source

Merge branch 'develop' into feature/bma/readReceipts

pull/1834/head
Benoit Marty 10 months ago committed by GitHub
parent
commit
58012268f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .maestro/tests/settings/settings.yaml
  2. 2
      app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
  3. 1
      changelog.d/1824.misc
  4. 29
      features/cachecleaner/api/build.gradle.kts
  5. 26
      features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt
  6. 25
      features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt
  7. 29
      features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt
  8. 41
      features/cachecleaner/impl/build.gradle.kts
  9. 59
      features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt
  10. 71
      features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt
  11. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  12. 106
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  13. 53
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt
  14. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt
  15. 1
      features/messages/impl/src/main/res/values-ru/translations.xml
  16. 1
      features/messages/impl/src/main/res/values-sk/translations.xml
  17. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
  18. 29
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
  19. 31
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt
  20. 104
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt
  21. 1
      features/preferences/impl/src/main/res/values-ru/translations.xml
  22. 1
      features/preferences/impl/src/main/res/values-sk/translations.xml
  23. 2
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
  24. 4
      features/rageshake/impl/src/main/res/values-ru/translations.xml
  25. 4
      features/rageshake/impl/src/main/res/values/localazy.xml
  26. 1
      features/roomdetails/impl/src/main/res/values-ru/translations.xml
  27. 1
      features/roomdetails/impl/src/main/res/values-sk/translations.xml
  28. 12
      gradle/libs.versions.toml
  29. 21
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt
  30. 14
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt
  31. 15
      libraries/ui-strings/src/main/res/values-ru/translations.xml
  32. 15
      libraries/ui-strings/src/main/res/values-sk/translations.xml
  33. BIN
      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
  34. BIN
      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
  35. BIN
      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
  36. BIN
      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
  37. BIN
      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
  38. BIN
      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
  39. BIN
      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
  40. BIN
      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

2
.maestro/tests/settings/settings.yaml

@ -16,7 +16,7 @@ appId: ${APP_ID} @@ -16,7 +16,7 @@ appId: ${APP_ID}
- tapOn:
text: "Report a problem"
- assertVisible: "Report a bug"
- assertVisible: "Report a problem"
- back
- tapOn:

2
app/src/main/kotlin/io/element/android/x/ElementXApplication.kt

@ -18,6 +18,7 @@ package io.element.android.x @@ -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 { @@ -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()
}

1
changelog.d/1824.misc

@ -0,0 +1 @@ @@ -0,0 +1 @@
Suppress usage of removeTimeline method.

29
features/cachecleaner/api/build.gradle.kts

@ -0,0 +1,29 @@ @@ -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)
}

26
features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt

@ -0,0 +1,26 @@ @@ -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()
}

25
features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt

@ -0,0 +1,25 @@ @@ -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
}

29
features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt

@ -0,0 +1,29 @@ @@ -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<Unit> {
override fun create(context: Context) {
context.bindings<CacheCleanerBindings>().cacheCleaner().clearCache()
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

41
features/cachecleaner/impl/build.gradle.kts

@ -0,0 +1,41 @@ @@ -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)
}

59
features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt

@ -0,0 +1,59 @@ @@ -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")
}
}
}
}

71
features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt

@ -0,0 +1,71 @@ @@ -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,
)
}

3
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 @@ -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( @@ -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<TimelineState> {
private val timeline = room.timeline
@ -159,6 +161,7 @@ class TimelinePresenter @Inject constructor( @@ -159,6 +161,7 @@ class TimelinePresenter @Inject constructor(
paginateBackwards()
}
}
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
.launchIn(this)
}

106
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -421,22 +421,6 @@ private fun MessageEventBubbleContent( @@ -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( @@ -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( @@ -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( @@ -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()
}
}
}

53
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt

@ -0,0 +1,53 @@ @@ -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<MatrixTimelineItem>)
}
@ContributesBinding(RoomScope::class)
class DefaultRedactedVoiceMessageManager @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val mediaPlayer: MediaPlayer,
) : RedactedVoiceMessageManager {
override suspend fun onEachMatrixTimelineItem(timelineItems: List<MatrixTimelineItem>) {
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() }
}
}
}
}
}
}

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt

@ -89,7 +89,6 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor( @@ -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() }

1
features/messages/impl/src/main/res/values-ru/translations.xml

@ -45,6 +45,7 @@ @@ -45,6 +45,7 @@
<string name="screen_room_notification_settings_error_loading_settings">"Произошла ошибка при загрузке настроек уведомлений."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Не удалось восстановить режим по умолчанию, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Не удалось настроить режим, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
<string name="screen_room_reactions_show_less">"Показать меньше"</string>

1
features/messages/impl/src/main/res/values-sk/translations.xml

@ -45,6 +45,7 @@ @@ -45,6 +45,7 @@
<string name="screen_room_notification_settings_error_loading_settings">"Pri načítavaní nastavení oznámení došlo k chybe."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Nepodarilo sa nastaviť režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"V tejto miestnosti ma upozorniť na"</string>
<string name="screen_room_reactions_show_less">"Zobraziť menej"</string>

2
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 @@ -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 { @@ -655,6 +656,7 @@ class MessagesPresenterTest {
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
featureFlagService = FakeFeatureFlagService(),
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
)
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)

29
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 @@ -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 { @@ -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 { @@ -324,6 +349,7 @@ class TimelinePresenterTest {
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
featureFlagService = FakeFeatureFlagService(),
redactedVoiceMessageManager = redactedVoiceMessageManager,
)
}
@ -340,6 +366,7 @@ class TimelinePresenterTest { @@ -340,6 +366,7 @@ class TimelinePresenterTest {
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
featureFlagService = FakeFeatureFlagService(),
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
)
}
}

31
features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt

@ -0,0 +1,31 @@ @@ -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<List<MatrixTimelineItem>> = mutableListOf()
val invocations: List<List<MatrixTimelineItem>>
get() = _invocations
override suspend fun onEachMatrixTimelineItem(timelineItems: List<MatrixTimelineItem>) {
_invocations.add(timelineItems)
}
}

104
features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt

@ -0,0 +1,104 @@ @@ -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>(
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
),
)
)

1
features/preferences/impl/src/main/res/values-ru/translations.xml

@ -28,6 +28,7 @@ @@ -28,6 +28,7 @@
<string name="screen_notification_settings_enable_notifications">"Включить уведомления на данном устройстве"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Конфигурация не была исправлена, попробуйте еще раз."</string>
<string name="screen_notification_settings_group_chats">"Групповые чаты"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string>
<string name="screen_notification_settings_mentions_section_title">"Упоминания"</string>
<string name="screen_notification_settings_mode_all">"Все"</string>
<string name="screen_notification_settings_mode_mentions">"Упоминания"</string>

1
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ť."</stri @@ -30,6 +30,7 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť."</stri
<string name="screen_notification_settings_enable_notifications">"Povoliť oznámenia na tomto zariadení"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurácia nebola opravená, skúste to prosím znova."</string>
<string name="screen_notification_settings_group_chats">"Skupinové rozhovory"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."</string>
<string name="screen_notification_settings_mentions_section_title">"Zmienky"</string>
<string name="screen_notification_settings_mode_all">"Všetky"</string>
<string name="screen_notification_settings_mode_mentions">"Zmienky"</string>

2
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt

@ -64,7 +64,7 @@ fun BugReportView( @@ -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

4
features/rageshake/impl/src/main/res/values-ru/translations.xml

@ -4,8 +4,8 @@ @@ -4,8 +4,8 @@
<string name="screen_bug_report_contact_me">"Вы можете связаться со мной, если у Вас возникнут какие-либо дополнительные вопросы."</string>
<string name="screen_bug_report_contact_me_title">"Связаться со мной"</string>
<string name="screen_bug_report_edit_screenshot">"Редактировать снимок экрана"</string>
<string name="screen_bug_report_editor_description">"Пожалуйста, опишите ошибку. Что вы сделали? Что вы ожидали, что произойдет? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее."</string>
<string name="screen_bug_report_editor_placeholder">"Опишите ошибку…"</string>
<string name="screen_bug_report_editor_description">"Пожалуйста, опишите ошибку. Что вы сделали? Какое поведение вы ожидали? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее."</string>
<string name="screen_bug_report_editor_placeholder">"Опишите проблему…"</string>
<string name="screen_bug_report_editor_supporting">"Если возможно, пожалуйста, напишите описание на английском языке."</string>
<string name="screen_bug_report_include_crash_logs">"Отправка журналов сбоев"</string>
<string name="screen_bug_report_include_logs">"Разрешить ведение журналов"</string>

4
features/rageshake/impl/src/main/res/values/localazy.xml

@ -4,8 +4,8 @@ @@ -4,8 +4,8 @@
<string name="screen_bug_report_contact_me">"You may contact me if you have any follow up questions."</string>
<string name="screen_bug_report_contact_me_title">"Contact me"</string>
<string name="screen_bug_report_edit_screenshot">"Edit screenshot"</string>
<string name="screen_bug_report_editor_description">"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."</string>
<string name="screen_bug_report_editor_placeholder">"Describe the bug…"</string>
<string name="screen_bug_report_editor_description">"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."</string>
<string name="screen_bug_report_editor_placeholder">"Describe the problem…"</string>
<string name="screen_bug_report_editor_supporting">"If possible, please write the description in English."</string>
<string name="screen_bug_report_include_crash_logs">"Send crash logs"</string>
<string name="screen_bug_report_include_logs">"Allow logs"</string>

1
features/roomdetails/impl/src/main/res/values-ru/translations.xml

@ -36,6 +36,7 @@ @@ -36,6 +36,7 @@
<string name="screen_room_notification_settings_error_loading_settings">"Произошла ошибка при загрузке настроек уведомлений."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Не удалось восстановить режим по умолчанию, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Не удалось настроить режим, попробуйте еще раз."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
<string name="screen_dm_details_block_alert_action">"Заблокировать"</string>

1
features/roomdetails/impl/src/main/res/values-sk/translations.xml

@ -36,6 +36,7 @@ @@ -36,6 +36,7 @@
<string name="screen_room_notification_settings_error_loading_settings">"Pri načítavaní nastavení oznámení došlo k chybe."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Nepodarilo sa nastaviť režim, skúste to prosím znova."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v tejto miestnosti nedostanete upozornenie."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"V tejto miestnosti ma upozorniť na"</string>
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>

12
gradle/libs.versions.toml

@ -3,7 +3,7 @@ @@ -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" @@ -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" @@ -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" @@ -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" @@ -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"

21
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 @@ -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( @@ -56,16 +55,14 @@ class RoomContentForwarder(
val failedForwardingTo = mutableSetOf<RoomId>()
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( @@ -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<TimelineDiff>) = Unit
}
}

14
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt

@ -70,3 +70,17 @@ internal fun Room.backPaginationStatusFlow(): Flow<BackPaginationStatus> = @@ -70,3 +70,17 @@ internal fun Room.backPaginationStatusFlow(): Flow<BackPaginationStatus> =
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<TimelineDiff>) = Unit
}

15
libraries/ui-strings/src/main/res/values-ru/translations.xml

@ -9,12 +9,14 @@ @@ -9,12 +9,14 @@
<string name="a11y_play">"Воспроизвести"</string>
<string name="a11y_poll">"Опрос"</string>
<string name="a11y_poll_end">"Опрос завершен"</string>
<string name="a11y_read_receipts_single">"Прочитано %1$s"</string>
<string name="a11y_send_files">"Отправить файлы"</string>
<string name="a11y_show_password">"Показать пароль"</string>
<string name="a11y_start_call">"Начать звонок"</string>
<string name="a11y_user_menu">"Меню пользователя"</string>
<string name="a11y_voice_message_record">"Записать голосовое сообщение."</string>
<string name="a11y_voice_message_stop_recording">"Остановить запись"</string>
<string name="a11y_read_receipts_multiple">"Прочитано %1$s и %2$s"</string>
<string name="action_accept">"Разрешить"</string>
<string name="action_add_to_timeline">"Добавить в хронологию"</string>
<string name="action_back">"Назад"</string>
@ -81,6 +83,7 @@ @@ -81,6 +83,7 @@
<string name="action_start_verification">"Начать подтверждение"</string>
<string name="action_static_map_load">"Нажмите, чтобы загрузить карту"</string>
<string name="action_take_photo">"Сделать фото"</string>
<string name="action_tap_for_options">"Нажмите для просмотра вариантов"</string>
<string name="action_try_again">"Повторить попытку"</string>
<string name="action_view_source">"Показать источник"</string>
<string name="action_yes">"Да"</string>
@ -89,12 +92,14 @@ @@ -89,12 +92,14 @@
<string name="common_acceptable_use_policy">"Политика допустимого использования"</string>
<string name="common_advanced_settings">"Дополнительные параметры"</string>
<string name="common_analytics">"Аналитика"</string>
<string name="common_appearance">"Оформление"</string>
<string name="common_audio">"Аудио"</string>
<string name="common_bubbles">"Пузыри"</string>
<string name="common_chat_backup">"Резервная копия чатов"</string>
<string name="common_copyright">"Авторское право"</string>
<string name="common_creating_room">"Создание комнаты…"</string>
<string name="common_current_user_left_room">"Покинул комнату"</string>
<string name="common_dark">"Темная"</string>
<string name="common_decryption_error">"Ошибка расшифровки"</string>
<string name="common_developer_options">"Для разработчика"</string>
<string name="common_edited_suffix">"(изменено)"</string>
@ -113,6 +118,7 @@ @@ -113,6 +118,7 @@
<string name="common_install_apk_android">"Установить APK"</string>
<string name="common_invite_unknown_profile">"Идентификатор Matrix ID не найден, приглашение может быть не получено."</string>
<string name="common_leaving_room">"Покинуть комнату"</string>
<string name="common_light">"Светлая"</string>
<string name="common_link_copied_to_clipboard">"Ссылка скопирована в буфер обмена"</string>
<string name="common_loading">"Загрузка…"</string>
<string name="common_message">"Сообщение"</string>
@ -146,7 +152,10 @@ @@ -146,7 +152,10 @@
<string name="common_search_for_someone">"Поиск человека"</string>
<string name="common_search_results">"Результаты поиска"</string>
<string name="common_security">"Безопасность"</string>
<string name="common_seen_by">"Просмотрено"</string>
<string name="common_sending">"Отправка…"</string>
<string name="common_sending_failed">"Сбой отправки"</string>
<string name="common_sent">"Отправлено"</string>
<string name="common_server_not_supported">"Сервер не поддерживается"</string>
<string name="common_server_url">"Адрес сервера"</string>
<string name="common_settings">"Настройки"</string>
@ -157,6 +166,7 @@ @@ -157,6 +166,7 @@
<string name="common_success">"Успешно"</string>
<string name="common_suggestions">"Рекомендации"</string>
<string name="common_syncing">"Синхронизация"</string>
<string name="common_system">"Системная"</string>
<string name="common_text">"Текст"</string>
<string name="common_third_party_notices">"Уведомление о третьей стороне"</string>
<string name="common_thread">"Обсуждение"</string>
@ -198,6 +208,11 @@ @@ -198,6 +208,11 @@
<item quantity="few">"Ведено %1$d цифр"</item>
<item quantity="many">"Введено много цифр"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Прочитано %1$s и %2$d другим"</item>
<item quantity="few">"Прочитано %1$s и %2$d другими"</item>
<item quantity="many">"Прочитано %1$s и %2$d другими"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d участник"</item>
<item quantity="few">"%1$d участников"</item>

15
libraries/ui-strings/src/main/res/values-sk/translations.xml

@ -9,12 +9,14 @@ @@ -9,12 +9,14 @@
<string name="a11y_play">"Prehrať"</string>
<string name="a11y_poll">"Anketa"</string>
<string name="a11y_poll_end">"Ukončená anketa"</string>
<string name="a11y_read_receipts_single">"Prečítal/a %1$s"</string>
<string name="a11y_send_files">"Odoslať súbory"</string>
<string name="a11y_show_password">"Zobraziť heslo"</string>
<string name="a11y_start_call">"Začať hovor"</string>
<string name="a11y_user_menu">"Používateľské menu"</string>
<string name="a11y_voice_message_record">"Nahrať hlasovú správu."</string>
<string name="a11y_voice_message_stop_recording">"Zastaviť nahrávanie"</string>
<string name="a11y_read_receipts_multiple">"Prečítal/a %1$s a %2$s"</string>
<string name="action_accept">"Prijať"</string>
<string name="action_add_to_timeline">"Pridať na časovú os"</string>
<string name="action_back">"Späť"</string>
@ -81,6 +83,7 @@ @@ -81,6 +83,7 @@
<string name="action_start_verification">"Spustiť overovanie"</string>
<string name="action_static_map_load">"Ťuknutím načítate mapu"</string>
<string name="action_take_photo">"Urobiť fotku"</string>
<string name="action_tap_for_options">"Klepnutím získate možnosti"</string>
<string name="action_try_again">"Skúste to znova"</string>
<string name="action_view_source">"Zobraziť zdroj"</string>
<string name="action_yes">"Áno"</string>
@ -89,12 +92,14 @@ @@ -89,12 +92,14 @@
<string name="common_acceptable_use_policy">"Zásady prijateľného používania"</string>
<string name="common_advanced_settings">"Pokročilé nastavenia"</string>
<string name="common_analytics">"Analytika"</string>
<string name="common_appearance">"Vzhľad"</string>
<string name="common_audio">"Zvuk"</string>
<string name="common_bubbles">"Bubliny"</string>
<string name="common_chat_backup">"Záloha konverzácie"</string>
<string name="common_copyright">"Autorské práva"</string>
<string name="common_creating_room">"Vytváranie miestnosti…"</string>
<string name="common_current_user_left_room">"Opustil/a miestnosť"</string>
<string name="common_dark">"Tmavý"</string>
<string name="common_decryption_error">"Chyba dešifrovania"</string>
<string name="common_developer_options">"Možnosti pre vývojárov"</string>
<string name="common_edited_suffix">"(upravené)"</string>
@ -113,6 +118,7 @@ @@ -113,6 +118,7 @@
<string name="common_install_apk_android">"Inštalovať APK"</string>
<string name="common_invite_unknown_profile">"Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá."</string>
<string name="common_leaving_room">"Opustenie miestnosti"</string>
<string name="common_light">"Svetlý"</string>
<string name="common_link_copied_to_clipboard">"Odkaz bol skopírovaný do schránky"</string>
<string name="common_loading">"Načítava sa…"</string>
<string name="common_message">"Správa"</string>
@ -146,7 +152,10 @@ @@ -146,7 +152,10 @@
<string name="common_search_for_someone">"Vyhľadať niekoho"</string>
<string name="common_search_results">"Výsledky hľadania"</string>
<string name="common_security">"Bezpečnosť"</string>
<string name="common_seen_by">"Videné"</string>
<string name="common_sending">"Odosiela sa…"</string>
<string name="common_sending_failed">"Odoslanie zlyhalo"</string>
<string name="common_sent">"Odoslané"</string>
<string name="common_server_not_supported">"Server nie je podporovaný"</string>
<string name="common_server_url">"URL adresa servera"</string>
<string name="common_settings">"Nastavenia"</string>
@ -157,6 +166,7 @@ @@ -157,6 +166,7 @@
<string name="common_success">"Úspech"</string>
<string name="common_suggestions">"Návrhy"</string>
<string name="common_syncing">"Synchronizuje sa"</string>
<string name="common_system">"Systém"</string>
<string name="common_text">"Text"</string>
<string name="common_third_party_notices">"Oznámenia tretích strán"</string>
<string name="common_thread">"Vlákno"</string>
@ -198,6 +208,11 @@ @@ -198,6 +208,11 @@
<item quantity="few">"%1$d zadané číslice"</item>
<item quantity="other">"%1$d zadaných číslic"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Prečítal/a %1$s a %2$d ďalší"</item>
<item quantity="few">"Prečítal/a %1$s a %2$d ďalší"</item>
<item quantity="other">"Prečítal/a %1$s a %2$d ďalších"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d člen"</item>
<item quantity="few">"%1$d členovia"</item>

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.

BIN
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 (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save