Merge branch 'develop' into feature/valere/call/decline_timeline_rendering

This commit is contained in:
Valere 2026-05-11 11:21:02 +02:00
commit a478d87fc3
995 changed files with 7864 additions and 3674 deletions

View file

@ -70,6 +70,7 @@ import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.location.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomdirectory.map
@ -113,6 +114,8 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.AuthData
import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
import org.matrix.rustcomponents.sdk.BeaconInfoListener
import org.matrix.rustcomponents.sdk.BeaconInfoUpdate
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
@ -207,6 +210,15 @@ class RustMatrixClient(
analyticsService = analyticsService,
)
override val ownBeaconInfoUpdates = mxCallbackFlow {
val listener = object : BeaconInfoListener {
override fun onUpdate(update: BeaconInfoUpdate) {
trySend(update.map())
}
}
innerClient.subscribeToOwnBeaconInfoUpdates(listener)
}
override val sessionVerificationService = RustSessionVerificationService(
client = innerClient,
isSyncServiceReady = syncService.syncState.map { it == SyncState.Running },

View file

@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
@ -66,6 +67,7 @@ class RustMatrixAuthenticationService(
private val rustMatrixClientFactory: RustMatrixClientFactory,
private val passphraseGenerator: PassphraseGenerator,
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
private val enterpriseService: EnterpriseService,
) : MatrixAuthenticationService {
// Any existing Element Classic session that we want to try to import secrets from during login.
private var elementClassicSession: ElementClassicSession? = null
@ -269,6 +271,12 @@ class RustMatrixAuthenticationService(
additionalScopes = emptyList(),
)
val url = oAuthAuthorizationData.loginUrl()
.let {
enterpriseService.tweakMasUrl(
url = it,
homeserver = client.server() ?: client.homeserver(),
)
}
pendingOAuthAuthorizationData = oAuthAuthorizationData
OAuthDetails(url)
}.mapFailure { failure ->

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
import io.element.android.libraries.matrix.impl.room.location.liveLocationSharesFlow
import io.element.android.libraries.matrix.impl.room.location.map
import io.element.android.libraries.matrix.impl.room.location.timedByExpiry
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService
@ -72,6 +73,7 @@ import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
import org.matrix.rustcomponents.sdk.KnockRequestsListener
import org.matrix.rustcomponents.sdk.LiveLocationException
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate
import org.matrix.rustcomponents.sdk.SendQueueListener
@ -525,12 +527,22 @@ class JoinedRustRoom(
override suspend fun stopLiveLocationShare(): Result<Unit> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.stopLiveLocationShare()
}.mapFailure { throwable ->
when (throwable) {
is LiveLocationException -> throwable.map()
else -> throwable
}
}
}
override suspend fun sendLiveLocation(geoUri: String): Result<Unit> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.sendLiveLocation(geoUri)
}.mapFailure { throwable ->
when (throwable) {
is LiveLocationException -> throwable.map()
else -> throwable
}
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.location
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.location.BeaconInfoUpdate
import org.matrix.rustcomponents.sdk.BeaconInfoUpdate as RustBeaconInfoUpdate
fun RustBeaconInfoUpdate.map(): BeaconInfoUpdate {
return BeaconInfoUpdate(
roomId = RoomId(roomId),
beaconId = EventId(eventId),
isLive = live
)
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.location
import io.element.android.libraries.matrix.api.room.location.LiveLocationException
import org.matrix.rustcomponents.sdk.LiveLocationException as RustLiveLocationException
fun RustLiveLocationException.map(): LiveLocationException {
return when (this) {
is RustLiveLocationException.Network -> LiveLocationException.Network()
is RustLiveLocationException.NotLive -> LiveLocationException.NotLive()
else -> LiveLocationException.Other(this)
}
}

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.room.location
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.LastLocation
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
@ -41,9 +42,9 @@ fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
}
}
return callbackFlow {
val liveLocationShares = liveLocationsObserver()
val observer = liveLocationsObserver()
val shares: MutableList<LiveLocationShare> = ArrayList()
val taskHandle = liveLocationShares.subscribe(object : LiveLocationsListener {
val taskHandle = observer.subscribe(object : LiveLocationsListener {
override fun onUpdate(updates: List<LiveLocationShareUpdate>) {
for (update in updates) {
shares.applyUpdate(update)
@ -53,13 +54,14 @@ fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
})
awaitClose {
taskHandle.cancelAndDestroy()
liveLocationShares.destroy()
observer.destroy()
}
}.buffer(Channel.UNLIMITED)
}
private fun RustLiveLocationShare.into(): LiveLocationShare {
return LiveLocationShare(
beaconId = EventId(beaconId),
userId = UserId(userId),
lastLocation = lastLocation?.let {
LastLocation(
@ -69,6 +71,6 @@ private fun RustLiveLocationShare.into(): LiveLocationShare {
)
},
startTimestamp = startTs.toLong(),
endTimestamp = (startTs + timeout).toLong()
endTimestamp = (startTs + timeout).toLong(),
)
}

View file

@ -26,6 +26,8 @@ object RoomPowerLevelsValuesMapper {
roomAvatar = values.roomAvatar,
roomTopic = values.roomTopic,
spaceChild = values.spaceChild,
beacon = values.beacon,
beaconInfo = values.beaconInfo,
)
}
}

View file

@ -124,7 +124,7 @@ class RustSessionVerificationService(
this.listener = listener
}
override suspend fun requestCurrentSessionVerification() = tryOrFail {
override suspend fun requestDeviceVerification() = tryOrFail {
ensureEncryptionIsInitialized()
verificationController.requestDeviceVerification()
currentVerificationRequest = VerificationRequest.Outgoing.CurrentSession
@ -146,7 +146,7 @@ class RustSessionVerificationService(
override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() }
override suspend fun startVerification() = tryOrFail {
override suspend fun startSasVerification() = tryOrFail {
verificationController.startSasVerification()
}

View file

@ -9,6 +9,8 @@
package io.element.android.libraries.matrix.impl.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
@ -50,6 +52,7 @@ class RustMatrixAuthenticationServiceTest {
private fun TestScope.createRustMatrixAuthenticationService(
sessionStore: SessionStore = InMemorySessionStore(),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
): RustMatrixAuthenticationService {
val baseDirectory = File("/base")
val cacheDirectory = File("/cache")
@ -68,6 +71,7 @@ class RustMatrixAuthenticationServiceTest {
buildMeta = aBuildMeta(),
oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(),
),
enterpriseService = enterpriseService,
)
}
}

View file

@ -67,6 +67,7 @@ internal fun aRustNotificationRoomInfo(
joinedMembersCount: ULong = 2u,
isEncrypted: Boolean? = true,
isDirect: Boolean = false,
isDm: Boolean = false,
joinRule: JoinRule? = null,
isSpace: Boolean = false,
serviceMembers: List<UserId> = emptyList(),
@ -79,6 +80,7 @@ internal fun aRustNotificationRoomInfo(
joinedMembersCount = joinedMembersCount,
isEncrypted = isEncrypted,
isDirect = isDirect,
isDm = isDm,
joinRule = joinRule,
isSpace = isSpace,
serviceMembers = serviceMembers.map { it.value },

View file

@ -63,6 +63,7 @@ internal fun aRustRoomInfo(
isLowPriority: Boolean = false,
activeRoomCallConsensusIntent: RtcCallIntentConsensus = RtcCallIntentConsensus.None,
activeServiceMembersCount: Int = 0,
isDm: Boolean = false,
) = RoomInfo(
id = id,
displayName = displayName,
@ -103,4 +104,5 @@ internal fun aRustRoomInfo(
isLowPriority = isLowPriority,
activeRoomCallConsensusIntent = activeRoomCallConsensusIntent,
activeServiceMembersCount = activeServiceMembersCount.toULong(),
isDm = isDm,
)

View file

@ -24,6 +24,7 @@ internal fun aRustRoomMember(
isIgnored: Boolean = false,
role: RoomMemberRole = RoomMemberRole.USER,
membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
) = RoomMember(
userId = userId.value,
displayName = displayName,
@ -34,4 +35,5 @@ internal fun aRustRoomMember(
isIgnored = isIgnored,
suggestedRoleForPowerLevel = role,
membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
)

View file

@ -22,6 +22,8 @@ internal fun aRustRoomPowerLevelsValues(
roomAvatar: Long,
roomTopic: Long,
spaceChild: Long,
beacon: Long,
beaconInfo: Long,
) = RoomPowerLevelsValues(
ban = ban,
invite = invite,
@ -33,5 +35,7 @@ internal fun aRustRoomPowerLevelsValues(
roomName = roomName,
roomAvatar = roomAvatar,
roomTopic = roomTopic,
spaceChild = spaceChild
spaceChild = spaceChild,
beacon = beacon,
beaconInfo = beaconInfo,
)

View file

@ -19,6 +19,7 @@ import org.matrix.rustcomponents.sdk.SpaceRoom
internal fun aRustSpaceRoom(
roomId: RoomId = A_ROOM_ID,
isDirect: Boolean = false,
isDm: Boolean = false,
canonicalAlias: String? = null,
rawName: String? = null,
displayName: String = "",
@ -35,6 +36,7 @@ internal fun aRustSpaceRoom(
) = SpaceRoom(
roomId = roomId.value,
isDirect = isDirect,
isDm = isDm,
canonicalAlias = canonicalAlias,
rawName = rawName,
displayName = displayName,

View file

@ -32,4 +32,6 @@ fun defaultFfiRoomPowerLevelValues() = RoomPowerLevelsValues(
roomTopic = 50,
spaceChild = 50,
usersDefault = 0,
beacon = 0,
beaconInfo = 0,
)

View file

@ -11,6 +11,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
@ -24,9 +25,9 @@ class TimedLiveLocationSharesFlowTest {
@Test
fun `it keeps emitting shares for subsequent expiries without upstream changes`() = runTest {
val shares = listOf(
aLiveLocationShare(userId = "@alice:server", endTimestamp = 1_000),
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
aLiveLocationShare(userId = "@carol:server", endTimestamp = 3_000),
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 1_000),
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000),
aLiveLocationShare(userId = UserId("@carol:server"), endTimestamp = 3_000),
)
flowOf(shares)
@ -56,8 +57,8 @@ class TimedLiveLocationSharesFlowTest {
@Test
fun `it does not double-emit when a share is already expired on receipt`() = runTest {
val shares = listOf(
aLiveLocationShare(userId = "@alice:server", endTimestamp = 500),
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 500),
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000),
)
flowOf(shares)
@ -81,8 +82,8 @@ class TimedLiveLocationSharesFlowTest {
val upstream = MutableSharedFlow<List<LiveLocationShare>>(extraBufferCapacity = 1)
val initialShares = listOf(aLiveLocationShare(endTimestamp = 10_000))
val updatedShares = listOf(
aLiveLocationShare(userId = "@alice:server", endTimestamp = 10_000),
aLiveLocationShare(userId = "@bob:server", endTimestamp = 6_000),
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 10_000),
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 6_000),
)
upstream
@ -133,15 +134,3 @@ class TimedLiveLocationSharesFlowTest {
}
}
}
private fun aLiveLocationShare(
userId: String = "@user:server",
endTimestamp: Long,
): LiveLocationShare {
return LiveLocationShare(
userId = UserId(userId),
lastLocation = null,
startTimestamp = 0L,
endTimestamp = endTimestamp,
)
}

View file

@ -30,6 +30,8 @@ class RoomPowerLevelsValuesMapperTest {
roomAvatar = 9,
roomTopic = 10,
spaceChild = 11,
beacon = 12,
beaconInfo = 13,
)
)
).isEqualTo(
@ -44,6 +46,8 @@ class RoomPowerLevelsValuesMapperTest {
roomAvatar = 9,
roomTopic = 10,
spaceChild = 11,
beacon = 12,
beaconInfo = 13,
)
)
}