From d511b837a0c7b37391a848b15a50e2375fd57107 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:14:17 +0000 Subject: [PATCH 01/12] Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be85b30691..4e1329e3e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ 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" From f78ecee19f398891318a833252aa93d6880a357e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:14:05 +0000 Subject: [PATCH 02/12] Update lifecycle to v2.7.0-rc01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dadea5b685..0743ac7457 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" From d36943cc41b4a654bce33f66211a1b22fbf516ad Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 16 Nov 2023 14:11:00 +0100 Subject: [PATCH 03/12] Suppress usage of removeTimeline method (#1824) --- changelog.d/1824.misc | 1 + .../matrix/impl/room/RoomContentForwarder.kt | 19 ++++++------------- .../impl/timeline/RoomTimelineExtensions.kt | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 changelog.d/1824.misc 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/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..1c203e30d6 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) { @@ -78,8 +75,4 @@ class RoomContentForwarder( throw ForwardEventException(toRoomIds.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 +} From f80ec8a8038b39a9e581acc407354cdb0d137441 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:25:56 +0000 Subject: [PATCH 04/12] Update wysiwyg to v2.17.0 (#1827) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc2f1c74c7..6139803131 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ serialization_json = "1.6.0" 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" From 589eeb4a515b426025d7924e45077cedcff96bbe Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Nov 2023 17:53:34 +0100 Subject: [PATCH 05/12] Fix wrong list used when forwarding an Event to some rooms fails. --- .../android/libraries/matrix/impl/room/RoomContentForwarder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1c203e30d6..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 @@ -72,7 +72,7 @@ class RoomContentForwarder( } if (failedForwardingTo.isNotEmpty()) { - throw ForwardEventException(toRoomIds.toList()) + throw ForwardEventException(failedForwardingTo.toList()) } } } From 8bc2a4d3a5abdd5a56c63543117b7c9277220e55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:51:59 +0000 Subject: [PATCH 06/12] Update android.gradle.plugin to v8.1.4 (#1830) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6139803131..f009190dd2 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" From 63d03be28c7b207d817797a0f8e6f1c9127b69b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 07:58:41 +0000 Subject: [PATCH 07/12] Update dependency com.google.firebase:firebase-bom to v32.6.0 (#1831) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f009190dd2..b43b4e7072 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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 From a0f289592effbff07907e0ab1fff9cecf6a68219 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Nov 2023 17:53:45 +0100 Subject: [PATCH 08/12] Timeline : do not use SubcomposeLayout if not needed --- .../components/TimelineItemEventRow.kt | 106 ++++++++++-------- 1 file changed, 57 insertions(+), 49 deletions(-) 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 cfb81f71a7..d4fccc296b 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 @@ -383,22 +383,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 @@ -422,21 +406,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) @@ -445,24 +428,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) ) @@ -478,52 +461,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() + } } } From 99dbbc722ddee3d235125df981a35f06331186db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 10:44:00 +0000 Subject: [PATCH 09/12] Update dependency me.saket.telephoto:zoomable-image-coil to v0.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b43b4e7072..8e2fbff726 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" From eeb9b30d802e2310ac496c7be3003de4da6aac50 Mon Sep 17 00:00:00 2001 From: ElementBot <110224175+ElementBot@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:07:53 +0000 Subject: [PATCH 10/12] Sync Strings (#1839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync Strings from Localazy * Fix 'Report a problem' screen title --------- Co-authored-by: bmarty Co-authored-by: Jorge Martín --- .maestro/tests/settings/settings.yaml | 2 +- .../impl/src/main/res/values-ru/translations.xml | 1 + .../impl/src/main/res/values-sk/translations.xml | 1 + .../impl/src/main/res/values-ru/translations.xml | 1 + .../impl/src/main/res/values-sk/translations.xml | 1 + .../rageshake/impl/bugreport/BugReportView.kt | 2 +- .../impl/src/main/res/values-ru/translations.xml | 4 ++-- .../impl/src/main/res/values/localazy.xml | 4 ++-- .../impl/src/main/res/values-ru/translations.xml | 1 + .../impl/src/main/res/values-sk/translations.xml | 1 + .../src/main/res/values-ru/translations.xml | 15 +++++++++++++++ .../src/main/res/values-sk/translations.xml | 15 +++++++++++++++ .../ui-strings/src/main/res/values/localazy.xml | 14 ++++++++++++++ ...gReportView-Day-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...gReportView-Day-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...gReportView-Day-0_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...gReportView-Day-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...eportView-Night-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...eportView-Night-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...eportView-Night-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...eportView-Night-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- 21 files changed, 72 insertions(+), 22 deletions(-) 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/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/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/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/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 6eb51a87c8..c2f969551c 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -9,12 +9,14 @@ "Play" "Poll" "Ended poll" + "Read by %1$s" "Send files" "Show password" "Start a call" "User menu" "Record voice message." "Stop recording" + "Read by %1$s and %2$s" "Accept" "Add to timeline" "Back" @@ -81,6 +83,7 @@ "Start verification" "Tap to load map" "Take photo" + "Tap for options" "Try again" "View source" "Yes" @@ -89,12 +92,14 @@ "Acceptable use policy" "Advanced settings" "Analytics" + "Appearance" "Audio" "Bubbles" "Chat backup" "Copyright" "Creating room…" "Left room" + "Dark" "Decryption error" "Developer options" "(edited)" @@ -113,6 +118,7 @@ "Install APK" "This Matrix ID can\'t be found, so the invite might not be received." "Leaving room" + "Light" "Link copied to clipboard" "Loading…" "Message" @@ -146,7 +152,10 @@ "Search for someone" "Search results" "Security" + "Seen by" "Sending…" + "Sending failed" + "Sent" "Server not supported" "Server URL" "Settings" @@ -157,6 +166,7 @@ "Success" "Suggestions" "Syncing" + "System" "Text" "Third-party notices" "Thread" @@ -197,6 +207,10 @@ "%1$d digit entered" "%1$d digits entered" + + "Read by %1$s and %2$d other" + "Read by %1$s and %2$d others" + "%1$d member" "%1$d members" 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 From 28f4ccdf9fe560d3b4a01d7e5c0cb867e74cd93d Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 20 Nov 2023 10:48:11 +0100 Subject: [PATCH 11/12] Delete media caches on startup (#1807) Clear media caches on app startup --- .../element/android/x/ElementXApplication.kt | 2 + features/cachecleaner/api/build.gradle.kts | 29 ++++++++ .../features/cachecleaner/api/CacheCleaner.kt | 26 +++++++ .../cachecleaner/api/CacheCleanerBindings.kt | 25 +++++++ .../api/CacheCleanerInitializer.kt | 29 ++++++++ features/cachecleaner/impl/build.gradle.kts | 41 +++++++++++ .../cachecleaner/impl/DefaultCacheCleaner.kt | 59 +++++++++++++++ .../impl/DefaultCacheCleanerTest.kt | 71 +++++++++++++++++++ .../timeline/VoiceMessageMediaRepo.kt | 1 - 9 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 features/cachecleaner/api/build.gradle.kts create mode 100644 features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt create mode 100644 features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerBindings.kt create mode 100644 features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleanerInitializer.kt create mode 100644 features/cachecleaner/impl/build.gradle.kts create mode 100644 features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt create mode 100644 features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt 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/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/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() } From 95ae3eaaf228a3dd5053a2cb347867b1526c912a Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 20 Nov 2023 10:48:25 +0100 Subject: [PATCH 12/12] Stop voice message on redaction (#1826) As per product spec: Voice messages must stop playing when redacted. --- .../impl/timeline/TimelinePresenter.kt | 3 + .../timeline/RedactedVoiceMessageManager.kt | 53 +++++++++ .../messages/MessagesPresenterTest.kt | 2 + .../timeline/TimelinePresenterTest.kt | 31 +++++- .../FakeRedactedVoiceMessageManager.kt | 31 ++++++ .../RedactedVoiceMessageManagerTest.kt | 104 ++++++++++++++++++ 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManager.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeRedactedVoiceMessageManager.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt 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 0a0feedf65..c9dcbbc8ab 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.matrix.api.core.EventId @@ -63,6 +64,7 @@ class TimelinePresenter @Inject constructor( private val analyticsService: AnalyticsService, private val verificationService: SessionVerificationService, private val encryptionService: EncryptionService, + private val redactedVoiceMessageManager: RedactedVoiceMessageManager, ) : Presenter { private val timeline = room.timeline @@ -142,6 +144,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/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/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 2969f26580..88468e6857 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 @@ -47,6 +47,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 @@ -653,6 +654,7 @@ class MessagesPresenterTest { analyticsService = analyticsService, encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), + 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 eeca4026bf..fc60fbf759 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 @@ -24,11 +24,14 @@ import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aTimelineItemsFactory -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter 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.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 @@ -310,9 +313,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, @@ -322,6 +347,7 @@ class TimelinePresenterTest { analyticsService = FakeAnalyticsService(), encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), + redactedVoiceMessageManager = redactedVoiceMessageManager, ) } @@ -337,6 +363,7 @@ class TimelinePresenterTest { analyticsService = analyticsService, encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), + 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 + ), + ) +)