Merge branch 'develop' into feature/bma/readReceipts
This commit is contained in:
commit
58012268f4
40 changed files with 646 additions and 93 deletions
|
|
@ -16,7 +16,7 @@ appId: ${APP_ID}
|
|||
|
||||
- tapOn:
|
||||
text: "Report a problem"
|
||||
- assertVisible: "Report a bug"
|
||||
- assertVisible: "Report a problem"
|
||||
- back
|
||||
|
||||
- tapOn:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
1
changelog.d/1824.misc
Normal file
1
changelog.d/1824.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Suppress usage of removeTimeline method.
|
||||
29
features/cachecleaner/api/build.gradle.kts
Normal file
29
features/cachecleaner/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Normal file
41
features/cachecleaner/impl/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import im.vector.app.features.analytics.plan.PollVote
|
|||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -67,6 +68,7 @@ class TimelinePresenter @Inject constructor(
|
|||
private val verificationService: SessionVerificationService,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||
) : Presenter<TimelineState> {
|
||||
|
||||
private val timeline = room.timeline
|
||||
|
|
@ -159,6 +161,7 @@ class TimelinePresenter @Inject constructor(
|
|||
paginateBackwards()
|
||||
}
|
||||
}
|
||||
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -421,22 +421,6 @@ private fun MessageEventBubbleContent(
|
|||
// to its `combinedClickable` parent so we do it manually
|
||||
fun onTimestampLongClick() = onMessageLongClick()
|
||||
|
||||
@Composable
|
||||
fun ContentView(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
isMine = event.isMine,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onMessageClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
eventSink = eventSink,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThreadDecoration(
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -460,21 +444,20 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ContentAndTimestampView(
|
||||
fun WithTimestampLayout(
|
||||
timestampPosition: TimestampPosition,
|
||||
modifier: Modifier = Modifier,
|
||||
contentModifier: Modifier = Modifier,
|
||||
timestampModifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
when (timestampPosition) {
|
||||
TimestampPosition.Overlay ->
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
content()
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
|
||||
.align(Alignment.BottomEnd)
|
||||
|
|
@ -483,24 +466,24 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
TimestampPosition.Aligned ->
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
content()
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
TimestampPosition.Below ->
|
||||
Column(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
content()
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
|
|
@ -516,52 +499,77 @@ private fun MessageEventBubbleContent(
|
|||
inReplyToDetails: InReplyTo.Ready?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val modifierWithPadding: Modifier
|
||||
val timestampLayoutModifier: Modifier
|
||||
val contentModifier: Modifier
|
||||
when {
|
||||
inReplyToDetails != null -> {
|
||||
if (timestampPosition == TimestampPosition.Overlay) {
|
||||
modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
timestampLayoutModifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
|
||||
} else {
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||
modifierWithPadding = Modifier
|
||||
timestampLayoutModifier = Modifier
|
||||
}
|
||||
}
|
||||
timestampPosition != TimestampPosition.Overlay -> {
|
||||
modifierWithPadding = Modifier
|
||||
timestampLayoutModifier = Modifier
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
}
|
||||
else -> {
|
||||
modifierWithPadding = Modifier
|
||||
timestampLayoutModifier = Modifier
|
||||
contentModifier = Modifier
|
||||
}
|
||||
}
|
||||
|
||||
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
||||
val threadDecoration = @Composable {
|
||||
if (showThreadDecoration) {
|
||||
ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp))
|
||||
}
|
||||
if (inReplyToDetails != null) {
|
||||
val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
|
||||
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
|
||||
val text = textForInReplyTo(inReplyToDetails)
|
||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||
ReplyToContent(
|
||||
senderName = senderName,
|
||||
text = text,
|
||||
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||
modifier = Modifier
|
||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable(enabled = true, onClick = inReplyToClick),
|
||||
}
|
||||
val contentWithTimestamp = @Composable {
|
||||
WithTimestampLayout(
|
||||
timestampPosition = timestampPosition,
|
||||
modifier = timestampLayoutModifier,
|
||||
) {
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
isMine = event.isMine,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onMessageClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
)
|
||||
}
|
||||
ContentAndTimestampView(
|
||||
timestampPosition = timestampPosition,
|
||||
modifier = modifierWithPadding,
|
||||
contentModifier = contentModifier,
|
||||
}
|
||||
val inReplyTo = @Composable { inReplyToReady: InReplyTo.Ready ->
|
||||
val senderName = inReplyToReady.senderDisplayName ?: inReplyToReady.senderId.value
|
||||
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToReady)
|
||||
val text = textForInReplyTo(inReplyToReady)
|
||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||
ReplyToContent(
|
||||
senderName = senderName,
|
||||
text = text,
|
||||
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||
modifier = Modifier
|
||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable(enabled = true, onClick = inReplyToClick),
|
||||
)
|
||||
|
||||
}
|
||||
if (inReplyToDetails != null) {
|
||||
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
|
||||
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
||||
threadDecoration()
|
||||
inReplyTo(inReplyToDetails)
|
||||
contentWithTimestamp()
|
||||
}
|
||||
} else {
|
||||
Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) {
|
||||
threadDecoration()
|
||||
contentWithTimestamp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import io.element.android.features.messages.test.FakeMessageComposerContext
|
|||
import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory
|
||||
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
|
||||
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||
import io.element.android.features.messages.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -655,6 +656,7 @@ class MessagesPresenterTest {
|
|||
encryptionService = FakeEncryptionService(),
|
||||
verificationService = FakeSessionVerificationService(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||
)
|
||||
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
|
||||
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||
import io.element.android.features.messages.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||
import io.element.android.features.messages.voicemessages.timeline.aRedactedMatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
|
|
@ -311,9 +314,31 @@ class TimelinePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - side effect on redacted items is invoked`() = runTest {
|
||||
val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager()
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = aRedactedMatrixTimeline(AN_EVENT_ID),
|
||||
),
|
||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1) // skip initial state
|
||||
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0)
|
||||
awaitItem().let {
|
||||
assertThat(it.timelineItems).isNotEmpty()
|
||||
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createTimelinePresenter(
|
||||
timeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory()
|
||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||
): TimelinePresenter {
|
||||
return TimelinePresenter(
|
||||
timelineItemsFactory = timelineItemsFactory,
|
||||
|
|
@ -324,6 +349,7 @@ class TimelinePresenterTest {
|
|||
encryptionService = FakeEncryptionService(),
|
||||
verificationService = FakeSessionVerificationService(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +366,7 @@ class TimelinePresenterTest {
|
|||
encryptionService = FakeEncryptionService(),
|
||||
verificationService = FakeSessionVerificationService(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,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,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
[versions]
|
||||
# Project
|
||||
android_gradle_plugin = "8.1.3"
|
||||
android_gradle_plugin = "8.1.4"
|
||||
kotlin = "1.9.20"
|
||||
ksp = "1.9.20-1.0.14"
|
||||
firebaseAppDistribution = "4.0.1"
|
||||
|
|
@ -13,7 +13,7 @@ core = "1.12.0"
|
|||
datastore = "1.0.0"
|
||||
constraintlayout = "2.1.4"
|
||||
constraintlayout_compose = "1.0.1"
|
||||
lifecycle = "2.7.0-beta01"
|
||||
lifecycle = "2.7.0-rc01"
|
||||
activity = "1.8.1"
|
||||
media3 = "1.1.1"
|
||||
|
||||
|
|
@ -33,11 +33,11 @@ test_core = "1.5.0"
|
|||
#other
|
||||
coil = "2.5.0"
|
||||
datetime = "0.4.1"
|
||||
serialization_json = "1.6.0"
|
||||
serialization_json = "1.6.1"
|
||||
showkase = "1.0.2"
|
||||
appyx = "1.4.0"
|
||||
sqldelight = "2.0.0"
|
||||
wysiwyg = "2.16.0"
|
||||
wysiwyg = "2.17.0"
|
||||
|
||||
# DI
|
||||
dagger = "2.48.1"
|
||||
|
|
@ -59,7 +59,7 @@ android_desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
|||
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
gms_google_services = "com.google.gms:google-services:4.4.0"
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:32.5.0"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:32.6.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
|
||||
# AndroidX
|
||||
|
|
@ -155,7 +155,7 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
|||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
|
||||
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.6.2"
|
||||
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.7.1"
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.0"
|
||||
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2"
|
||||
|
|
|
|||
|
|
@ -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<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(
|
|||
}
|
||||
|
||||
if (failedForwardingTo.isNotEmpty()) {
|
||||
throw ForwardEventException(toRoomIds.toList())
|
||||
throw ForwardEventException(failedForwardingTo.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private object NoOpTimelineListener : TimelineListener {
|
||||
override fun onUpdate(diff: List<TimelineDiff>) = Unit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf178a51be45cfd5fee676628ae3192374c1bc873e6eef726053ae593974aafe
|
||||
size 68108
|
||||
oid sha256:3928ea1a60bf2b5c1cc94ef566ca67ce03679994430f826f50636c781976e8e2
|
||||
size 70068
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:922ffb470cc64a29cd2d9a65840d267cb742efa523f3d93676ec148f818850e1
|
||||
size 204829
|
||||
oid sha256:62ddb0f9d6764b639b1a1438535c19138d1f81afdaaebfe03b021fc7f64a5291
|
||||
size 206670
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:332e7a7baa12fb54fed4ad99f53470a96182e6059f3f24631ef9c5a7ed9c9f54
|
||||
size 59420
|
||||
oid sha256:6d546a6a3a5e517e99758f05396a72cc7bf802f3e207f5c7d881e5805f103b67
|
||||
size 61356
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf178a51be45cfd5fee676628ae3192374c1bc873e6eef726053ae593974aafe
|
||||
size 68108
|
||||
oid sha256:3928ea1a60bf2b5c1cc94ef566ca67ce03679994430f826f50636c781976e8e2
|
||||
size 70068
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:310bd5d524d61cbc3b8237811c8d6623a6fb1046bb8a9823ff5173c2f2b3c862
|
||||
size 65218
|
||||
oid sha256:53cb4046ffb2391e880bb9f4567534483f9c3e8bdbea7d7706d3fb93ec0acdec
|
||||
size 67105
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3bd787f27931a5f71da9ed4248ff7c310acce37168b36e2575995ea1a1dc2cac
|
||||
size 200484
|
||||
oid sha256:22c01def033c5ce1e7bc12dc7b5ec6fae11bb827f29818d3921f156a4cf04386
|
||||
size 202460
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e93d40807105a64a933180a71a73c661c4b792f5a008af7ada5fc8fd80a1446b
|
||||
size 54960
|
||||
oid sha256:8960e097e94be82ab519bbc1ed3679955dd200349ae32e55d11fad1cbaf586ae
|
||||
size 56261
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:310bd5d524d61cbc3b8237811c8d6623a6fb1046bb8a9823ff5173c2f2b3c862
|
||||
size 65218
|
||||
oid sha256:53cb4046ffb2391e880bb9f4567534483f9c3e8bdbea7d7706d3fb93ec0acdec
|
||||
size 67105
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue