mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge branch 'develop' into feature/bma/otherAccountUnreadCount
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -84,5 +84,9 @@ fun File.getSizeOfFiles(): Long {
|
||||
Timber.v("Get size of ${it.absolutePath}")
|
||||
true
|
||||
}
|
||||
.sumOf { it.length() }
|
||||
// Only sum the actual size of files, not directories, to avoid double counting or worse,
|
||||
// since the docs mention that `length()` for directories is not reliable.
|
||||
// For directories, we can assume a size of 4096 bytes (4 KiB) as a placeholder,
|
||||
// which is a common block size on many file systems.
|
||||
.sumOf { if (it.isFile) it.length() else 4096L }
|
||||
}
|
||||
|
||||
+13
-28
@@ -15,6 +15,8 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.awaitLongPressOrCancellation
|
||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -53,8 +55,7 @@ import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
|
||||
import androidx.compose.ui.input.pointer.changedToUp
|
||||
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
@@ -565,34 +566,18 @@ private fun Modifier.linkTapHandler(
|
||||
val (url, urlText) = layoutResult.value?.urlAt(down.position, text) ?: return@awaitEachGesture
|
||||
down.consume()
|
||||
|
||||
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
|
||||
|
||||
// Try detecting a long press by waiting for the long press timeout and checking if we received an up event before that.
|
||||
// If we did, it's a tap, otherwise it's a long press.
|
||||
// This warning is a false positive.
|
||||
@Suppress("KotlinConstantConditions")
|
||||
val isLongPress = try {
|
||||
withTimeout(longPressTimeout) {
|
||||
var receivedUp = false
|
||||
while (!receivedUp) {
|
||||
val event = awaitPointerEvent()
|
||||
val up = event.changes.firstOrNull { it.id == down.id && it.changedToUp() }
|
||||
if (up != null) {
|
||||
receivedUp = true
|
||||
up.consume()
|
||||
}
|
||||
}
|
||||
|
||||
!receivedUp
|
||||
}
|
||||
} catch (_: PointerEventTimeoutCancellationException) {
|
||||
true
|
||||
}
|
||||
|
||||
if (isLongPress) {
|
||||
// Returns a non-null change if the long press timeout elapsed, or null if the gesture was canceled by movement beyond touch slop or by an up event.
|
||||
if (awaitLongPressOrCancellation(down.id) != null) {
|
||||
context.onLinkLongClick(url, urlText)
|
||||
// Swallow the rest of the gesture so parents don't react to the eventual release.
|
||||
waitForUpOrCancellation()?.consume()
|
||||
} else {
|
||||
context.onLinkClick(url, urlText)
|
||||
// Either a tap (the pointer was released) or a drag (the user probably wanted to scroll).
|
||||
val up = currentEvent.changes.firstOrNull { it.id == down.id && it.changedToUpIgnoreConsumed() }
|
||||
if (up != null) {
|
||||
up.consume()
|
||||
context.onLinkClick(url, urlText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,11 @@ dependencies {
|
||||
implementation(platform("io.element.android:element-call-bom:$elementCallVersion"))
|
||||
implementation(libs.element.call.api)
|
||||
implementation(projects.features.callnative.api)
|
||||
implementation(libs.element.call.matrix)
|
||||
implementation(libs.element.call.matrix) {
|
||||
if (usesLocalRustSdk) {
|
||||
exclude(group = "org.matrix.rustcomponents", module = "sdk-android")
|
||||
}
|
||||
}
|
||||
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.features.enterprise.api)
|
||||
|
||||
+4
-1
@@ -118,6 +118,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
@@ -320,7 +321,7 @@ class RustMatrixClient(
|
||||
override fun onUpdate(profile: UserProfile) {
|
||||
val matrixUser = profile.map()
|
||||
_userProfile.tryEmit(matrixUser)
|
||||
sessionCoroutineScope.launch {
|
||||
sessionCoroutineScope.launch(sessionDispatcher) {
|
||||
storeUserProfile(matrixUser)
|
||||
}
|
||||
}
|
||||
@@ -1013,6 +1014,8 @@ class RustMatrixClient(
|
||||
searchBackfillService.cancelPeriodicSearchBackfill()
|
||||
}
|
||||
}
|
||||
// Checking the pending work is a blocking call, do not do it on the main thread
|
||||
.flowOn(sessionDispatcher)
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -158,6 +158,7 @@ class JoinedRustRoom(
|
||||
inner = innerRoom.threadListService(),
|
||||
contentMapper = TimelineEventContentMapper(),
|
||||
roomCoroutineScope = roomCoroutineScope,
|
||||
roomDispatcher = roomDispatcher,
|
||||
)
|
||||
|
||||
override val syncUpdateFlow = flow {
|
||||
|
||||
+5
@@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.api.room.threads.ThreadsListService
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.map
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -33,6 +35,7 @@ import org.matrix.rustcomponents.sdk.ThreadListService as InnerThreadListService
|
||||
class RustThreadsListService(
|
||||
private val inner: InnerThreadListService,
|
||||
private val roomCoroutineScope: CoroutineScope,
|
||||
private val roomDispatcher: CoroutineDispatcher,
|
||||
private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(),
|
||||
) : ThreadsListService {
|
||||
private var itemSubscriptionJob: Job? = null
|
||||
@@ -63,6 +66,8 @@ class RustThreadsListService(
|
||||
updated.apply(diff, contentMapper)
|
||||
items.value = updated
|
||||
}
|
||||
// Fetching and mapping the items can be costly, do not do it on the main thread
|
||||
.flowOn(roomDispatcher)
|
||||
.launchIn(roomCoroutineScope)
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -33,7 +33,9 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
@@ -41,6 +43,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import java.io.File
|
||||
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RustMatrixClientFactoryTest {
|
||||
@Test
|
||||
fun test() = runTest {
|
||||
@@ -51,6 +54,7 @@ class RustMatrixClientFactoryTest {
|
||||
val result = sut.create(aSessionData())
|
||||
|
||||
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
|
||||
runCurrent()
|
||||
// 2 workers are scheduled: one for the search backfill and one for the vacuum
|
||||
scheduleWorkersLambda.assertions().isCalledExactly(2)
|
||||
result.destroy()
|
||||
|
||||
+2
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -122,6 +123,7 @@ class RustThreadsListServiceTest {
|
||||
) = RustThreadsListService(
|
||||
inner = inner,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
roomDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
|
||||
private fun aRustThreadListUpdate() = ThreadListUpdate.Append(
|
||||
|
||||
+7
-1
@@ -10,6 +10,7 @@ package io.element.android.libraries.network.interceptors
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -31,7 +32,12 @@ class DynamicHttpLoggingInterceptor(
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
// This is called in a separate thread, so calling `runBlocking` here should be fine, it should be also instant after the value is cached
|
||||
val logLevel = runBlocking { appPreferencesStore.getTracingLogLevelFlow().first() }
|
||||
loggingInterceptor.level = if (logLevel >= LogLevel.DEBUG) Level.BODY else Level.NONE
|
||||
// If the request body is binary, we log only the headers to avoid logging binary data
|
||||
val isBinaryBody = chain.request().headers["Content-Type"] == MimeTypes.OctetStream
|
||||
loggingInterceptor.level = when {
|
||||
logLevel >= LogLevel.DEBUG -> if (isBinaryBody) Level.HEADERS else Level.BODY
|
||||
else -> Level.NONE
|
||||
}
|
||||
return loggingInterceptor.intercept(chain)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-21
@@ -14,6 +14,7 @@ import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -23,23 +24,25 @@ import io.element.android.libraries.push.impl.PushDatabase
|
||||
import io.element.android.libraries.push.impl.db.PushHistory
|
||||
import io.element.android.libraries.push.impl.db.PushRequest
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Instant
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHistoryService(
|
||||
private val pushDatabase: PushDatabase,
|
||||
private val systemClock: SystemClock,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
@ApplicationContext context: Context,
|
||||
) : PushHistoryService {
|
||||
private val powerManager = context.getSystemService<PowerManager>()
|
||||
private val packageName = context.packageName
|
||||
|
||||
override suspend fun insertOrUpdatePushRequest(pushRequest: PushRequest): Result<Unit> {
|
||||
return runCatchingExceptions { pushDatabase.pushRequestQueries.insertPushRequest(pushRequest).await() }
|
||||
override suspend fun insertOrUpdatePushRequest(pushRequest: PushRequest): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions<Unit> { pushDatabase.pushRequestQueries.insertPushRequest(pushRequest).await() }
|
||||
}
|
||||
|
||||
override suspend fun insertOrUpdatePushRequests(pushRequests: List<PushRequest>): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
override suspend fun insertOrUpdatePushRequests(pushRequests: List<PushRequest>): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
pushDatabase.transaction {
|
||||
for (request in pushRequests) {
|
||||
pushDatabase.pushRequestQueries.insertPushRequest(request)
|
||||
@@ -48,8 +51,8 @@ class DefaultPushHistoryService(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPendingPushRequests(sessionId: SessionId, since: Instant?): Result<List<PushRequest>> {
|
||||
return runCatchingExceptions {
|
||||
override suspend fun getPendingPushRequests(sessionId: SessionId, since: Instant?): Result<List<PushRequest>> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
pushDatabase.transactionWithResult {
|
||||
val sinceTimeMillis = since?.toEpochMilliseconds() ?: 0
|
||||
pushDatabase.pushRequestQueries.selectAllPendingForSession(sessionId.value, sinceTimeMillis).executeAsList()
|
||||
@@ -57,14 +60,14 @@ class DefaultPushHistoryService(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
override suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions<Unit> {
|
||||
val keepAmount = 100L
|
||||
pushDatabase.pushRequestQueries.removeOldest(keepAmount)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPushResult(
|
||||
override suspend fun onPushResult(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
@@ -91,19 +94,21 @@ class DefaultPushHistoryService(
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
|
||||
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||
PushHistory(
|
||||
pushDate = systemClock.epochMillis(),
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId?.value,
|
||||
roomId = roomId?.value,
|
||||
sessionId = sessionId?.value,
|
||||
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||
comment = finalComment,
|
||||
withContext(dispatchers.io) {
|
||||
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||
PushHistory(
|
||||
pushDate = systemClock.epochMillis(),
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId?.value,
|
||||
roomId = roomId?.value,
|
||||
sessionId = sessionId?.value,
|
||||
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||
comment = finalComment,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Keep only the last 1_000 events
|
||||
pushDatabase.pushHistoryQueries.removeOldest(1_000)
|
||||
// Keep only the last 1_000 events
|
||||
pushDatabase.pushHistoryQueries.removeOldest(1_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -20,7 +20,7 @@ interface PushHistoryService {
|
||||
* Create a new push history entry.
|
||||
* Do not use directly, prefer using the extension functions.
|
||||
*/
|
||||
fun onPushResult(
|
||||
suspend fun onPushResult(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
@@ -52,7 +52,7 @@ interface PushHistoryService {
|
||||
suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit>
|
||||
}
|
||||
|
||||
fun PushHistoryService.onInvalidPushReceived(
|
||||
suspend fun PushHistoryService.onInvalidPushReceived(
|
||||
providerInfo: String,
|
||||
data: String,
|
||||
) = onPushResult(
|
||||
@@ -65,7 +65,7 @@ fun PushHistoryService.onInvalidPushReceived(
|
||||
comment = "Invalid or ignored push data:\n$data",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToRetrieveSession(
|
||||
suspend fun PushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
@@ -80,7 +80,7 @@ fun PushHistoryService.onUnableToRetrieveSession(
|
||||
comment = "Unable to retrieve session: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToResolveEvent(
|
||||
suspend fun PushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
@@ -96,7 +96,7 @@ fun PushHistoryService.onUnableToResolveEvent(
|
||||
comment = "Unable to resolve event: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onSuccess(
|
||||
suspend fun PushHistoryService.onSuccess(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
@@ -117,7 +117,7 @@ fun PushHistoryService.onSuccess(
|
||||
},
|
||||
)
|
||||
|
||||
fun PushHistoryService.onDiagnosticPush(
|
||||
suspend fun PushHistoryService.onDiagnosticPush(
|
||||
providerInfo: String,
|
||||
) = onPushResult(
|
||||
providerInfo = providerInfo,
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class FakePushHistoryService(
|
||||
private val getPendingPushRequests: (SessionId, Instant?) -> Result<List<PushRequest>> = { _, _ -> lambdaError() },
|
||||
private val removeOldPushRequests: (SessionId) -> Result<Unit> = { lambdaError() },
|
||||
) : PushHistoryService {
|
||||
override fun onPushResult(
|
||||
override suspend fun onPushResult(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
|
||||
Reference in New Issue
Block a user