Merge branch 'develop' into feature/fga/permalink_timeline

This commit is contained in:
Benoit Marty 2024-04-26 12:50:38 +02:00
commit 2c8abbed0c
1157 changed files with 4307 additions and 1899 deletions

View file

@ -61,6 +61,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewMapper
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
@ -159,7 +160,6 @@ class RustMatrixClient(
syncService = rustSyncService,
sessionCoroutineScope = sessionCoroutineScope,
dispatchers = dispatchers,
sessionStore = sessionStore,
)
private val roomDirectoryService = RustRoomDirectoryService(
@ -187,7 +187,6 @@ class RustMatrixClient(
isTokenValid = false,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
needsVerification = existingData.needsVerification,
)
sessionStore.updateData(newData)
Timber.d("Removed session data with token: '...$anonymizedToken'.")
@ -215,7 +214,6 @@ class RustMatrixClient(
isTokenValid = true,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
needsVerification = existingData.needsVerification,
)
sessionStore.updateData(newData)
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
@ -241,7 +239,6 @@ class RustMatrixClient(
client = client,
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
sessionCoroutineScope = sessionCoroutineScope,
sessionStore = sessionStore,
)
private val eventFilters = TimelineConfig.excludedEvents
@ -439,7 +436,7 @@ class RustMatrixClient(
runCatching { client.removeAvatar() }
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
override suspend fun joinRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatching {
client.joinRoomById(roomId.value).destroy()
try {
@ -447,10 +444,13 @@ class RustMatrixClient(
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
}
roomId
}
}
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
return Result.failure(NotImplementedError("Not yet implemented"))
}
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatching {
client.trackRecentlyVisitedRoom(roomId.value)
@ -463,21 +463,15 @@ class RustMatrixClient(
}
}
@Suppress("TooGenericExceptionThrown")
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) {
runCatching {
// TODO Waiting for SDK to be released
throw Exception("Not implemented")
// client.resolveRoomAlias(roomAlias.value).let(::RoomId)
client.resolveRoomAlias(roomAlias.value).let(::RoomId)
}
}
@Suppress("TooGenericExceptionThrown")
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching {
// TODO Waiting for SDK to be released
throw Exception("Not implemented")
// client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
}
}

View file

@ -138,7 +138,6 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
needsVerification = true,
)
}
sessionStore.storeData(sessionData)
@ -187,7 +186,6 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
needsVerification = true,
)
}
pendingOidcAuthenticationData?.close()

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.currentCoroutineContext
@ -49,11 +48,10 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
internal class RustEncryptionService(
private val client: Client,
client: Client,
syncService: RustSyncService,
sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
) : EncryptionService {
private val service: Encryption = client.encryption()
@ -188,9 +186,6 @@ internal class RustEncryptionService(
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.recover(recoveryKey)
val existingSession = sessionStore.getSession(client.userId())
?: error("Failed to save verification state. No session with id ${client.userId()}")
sessionStore.updateData(existingSession.copy(needsVerification = false))
}.mapFailure {
it.mapRecoveryException()
}

View file

@ -25,7 +25,6 @@ internal fun Session.toSessionData(
isTokenValid: Boolean,
loginType: LoginType,
passphrase: String?,
needsVerification: Boolean,
) = SessionData(
userId = userId,
deviceId = deviceId,
@ -38,5 +37,4 @@ internal fun Session.toSessionData(
isTokenValid = isTokenValid,
loginType = loginType,
passphrase = passphrase,
needsVerification = needsVerification,
)

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.RoomType
fun String?.toRoomType(): RoomType {
return when (this) {
null -> RoomType.Room
"m.space" -> RoomType.Space
else -> RoomType.Other(this)
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.room.preview
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.impl.room.toRoomType
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
object RoomPreviewMapper {
fun map(roomPreview: RustRoomPreview): RoomPreview {
return RoomPreview(
roomId = RoomId(roomPreview.roomId),
canonicalAlias = roomPreview.canonicalAlias?.let(::RoomAlias),
name = roomPreview.name,
topic = roomPreview.topic,
avatarUrl = roomPreview.avatarUrl,
numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
roomType = roomPreview.roomType.toRoomType(),
isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
isJoined = roomPreview.isJoined,
isInvited = roomPreview.isInvited,
isPublic = roomPreview.isPublic,
canKnock = roomPreview.canKnock
)
}
}

View file

@ -40,7 +40,7 @@ val RoomListFilter.predicate
(roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread)
}
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true)
roomSummary is RoomSummary.Filled && roomSummary.details.name.orEmpty().contains(pattern, ignoreCase = true)
}
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited()

View file

@ -33,7 +33,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
}
return RoomSummaryDetails(
roomId = RoomId(roomInfo.id),
name = roomInfo.name ?: roomInfo.id,
name = roomInfo.name,
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl,

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
import io.element.android.libraries.matrix.impl.media.map
import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.ProfileDetails
import org.matrix.rustcomponents.sdk.RepliedToEventDetails
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
@ -51,13 +50,11 @@ class EventMessageMapper {
val inReplyToId = EventId(details.eventId)
when (val event = details.event) {
is RepliedToEventDetails.Ready -> {
val senderProfile = event.senderProfile as? ProfileDetails.Ready
InReplyTo.Ready(
eventId = inReplyToId,
content = timelineEventContentMapper.map(event.content),
senderId = UserId(event.sender),
senderDisplayName = senderProfile?.displayName,
senderAvatarUrl = senderProfile?.avatarUrl,
senderProfile = event.senderProfile.map(),
)
}
is RepliedToEventDetails.Error -> InReplyTo.Error

View file

@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@ -33,10 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -58,7 +53,6 @@ class RustSessionVerificationService(
private val client: Client,
isSyncServiceReady: Flow<Boolean>,
private val sessionCoroutineScope: CoroutineScope,
private val sessionStore: SessionStore,
) : SessionVerificationService, SessionVerificationControllerDelegate {
private val encryptionService: Encryption = client.encryption()
private lateinit var verificationController: SessionVerificationController
@ -80,11 +74,6 @@ class RustSessionVerificationService(
}
})
override val needsVerificationFlow: StateFlow<Boolean> = sessionStore.sessionsFlow()
.map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() }
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
override val verificationFlowState = _verificationFlowState.asStateFlow()
@ -98,6 +87,9 @@ class RustSessionVerificationService(
}
init {
// Update initial state in case sliding sync isn't ready
updateVerificationStatus(encryptionService.verificationState())
isReady.onEach { isReady ->
if (isReady) {
Timber.d("Starting verification service")
@ -165,7 +157,6 @@ class RustSessionVerificationService(
}
}
.onSuccess {
saveVerifiedState(true)
updateVerificationStatus(VerificationState.VERIFIED)
_verificationFlowState.value = VerificationFlowState.Finished
}
@ -195,14 +186,6 @@ class RustSessionVerificationService(
_verificationFlowState.value = VerificationFlowState.Initial
}
override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail {
val existingSession = sessionStore.getSession(client.userId())
?: error("Failed to save verification state. No session with id ${client.userId()}")
sessionStore.updateData(existingSession.copy(needsVerification = !verified))
// Wait until the new state is saved
needsVerificationFlow.first { needsVerification -> !needsVerification }
}
fun destroy() {
Timber.d("Destroying RustSessionVerificationService")
verificationStateListenerTaskHandle.cancelAndDestroy()

View file

@ -22,13 +22,20 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.services.analytics.test.FakeAnalyticsService
import org.junit.Test
import org.matrix.rustcomponents.sdk.UnableToDecryptInfo
import uniffi.matrix_sdk_crypto.UtdCause
class UtdTrackerTest {
@Test
fun `when onUtd is called with null timeToDecryptMs, the expected analytics Event is sent`() {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = null))
sut.onUtd(
UnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = null,
cause = UtdCause.UNKNOWN,
)
)
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
Error(
context = null,
@ -47,7 +54,13 @@ class UtdTrackerTest {
fun `when onUtd is called with timeToDecryptMs, the expected analytics Event is sent`() {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = 123.toULong()))
sut.onUtd(
UnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.UNKNOWN,
)
)
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
Error(
context = null,