Merge branch 'develop' into feature/valere/message_shields

This commit is contained in:
Benoit Marty
2024-08-14 12:37:31 +02:00
committed by GitHub
342 changed files with 5475 additions and 1377 deletions

View File

@@ -23,10 +23,12 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
@@ -46,9 +48,18 @@ class RustMatrixClientFactory @Inject constructor(
private val proxyProvider: ProxyProvider,
private val clock: SystemClock,
private val utdTracker: UtdTracker,
private val appPreferencesStore: AppPreferencesStore,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder(sessionData.sessionPath, sessionData.passphrase)
val client = getBaseClientBuilder(
sessionPath = sessionData.sessionPath,
passphrase = sessionData.passphrase,
slidingSync = if (appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()) {
ClientBuilderSlidingSync.Simplified
} else {
ClientBuilderSlidingSync.Restored
},
)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.use { it.build() }
@@ -79,6 +90,7 @@ class RustMatrixClientFactory @Inject constructor(
sessionPath: String,
passphrase: String?,
slidingSyncProxy: String? = null,
slidingSync: ClientBuilderSlidingSync,
): ClientBuilder {
return ClientBuilder()
.sessionPath(sessionPath)
@@ -88,6 +100,13 @@ class RustMatrixClientFactory @Inject constructor(
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true)
.autoEnableCrossSigning(true)
.run {
when (slidingSync) {
ClientBuilderSlidingSync.Restored -> this
ClientBuilderSlidingSync.Discovered -> requiresSlidingSync()
ClientBuilderSlidingSync.Simplified -> simplifiedSlidingSync(true)
}
}
.run {
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
proxyProvider.provides()?.let { proxy(it) } ?: this
@@ -95,6 +114,17 @@ class RustMatrixClientFactory @Inject constructor(
}
}
enum class ClientBuilderSlidingSync {
// The proxy will be supplied when restoring the Session.
Restored,
// A proxy must be discovered whilst building the session.
Discovered,
// Use Simplified Sliding Sync (discovery isn't a thing yet).
Simplified,
}
private fun SessionData.toSession() = Session(
accessToken = accessToken,
refreshToken = refreshToken,

View File

@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
@@ -59,7 +60,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
baseDirectory: File,
private val baseDirectory: File,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
private val rustMatrixClientFactory: RustMatrixClientFactory,
@@ -69,10 +70,19 @@ class RustMatrixAuthenticationService @Inject constructor(
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
private val pendingPassphrase = getDatabasePassphrase()
private val sessionPath = File(baseDirectory, UUID.randomUUID().toString()).absolutePath
// Need to keep a copy of the current session path to eventually delete it.
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
private var sessionPath: File? = null
private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private fun rotateSessionPath(): File {
sessionPath?.deleteRecursively()
return File(baseDirectory, UUID.randomUUID().toString())
.also { sessionPath = it }
}
override fun loggedInStateFlow(): Flow<LoggedInState> {
return sessionStore.isLoggedIn()
}
@@ -116,8 +126,9 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
runCatching {
val client = getBaseClientBuilder()
val client = getBaseClientBuilder(emptySessionPath)
.serverNameOrHomeserverUrl(homeserver)
.build()
currentClient = client
@@ -134,13 +145,14 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = currentSessionPath.absolutePath,
)
clear()
sessionStore.storeData(sessionData)
@@ -184,13 +196,14 @@ class RustMatrixAuthenticationService @Inject constructor(
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first")
client.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = currentSessionPath.absolutePath,
)
clear()
pendingOidcAuthorizationData?.close()
@@ -205,11 +218,13 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPath = sessionPath,
sessionPath = emptySessionPath.absolutePath,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
@@ -227,7 +242,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = emptySessionPath.absolutePath,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
@@ -244,15 +259,17 @@ class RustMatrixAuthenticationService @Inject constructor(
}
Timber.e(throwable, "Failed to login with QR code")
}
}
}
private fun getBaseClientBuilder() = rustMatrixClientFactory
private fun getBaseClientBuilder(
sessionPath: File,
) = rustMatrixClientFactory
.getBaseClientBuilder(
sessionPath = sessionPath,
sessionPath = sessionPath.absolutePath,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)
.requiresSlidingSync()
private fun clear() {
currentClient?.close()

View File

@@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.core.EventId
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.core.UserId
@@ -58,7 +59,8 @@ class MatrixRoomInfoMapper {
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
hasRoomCall = it.hasRoomCall,
activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList(),
heroes = it.elementHeroes().toImmutableList()
heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
)
}
}

View File

@@ -51,23 +51,15 @@ class RoomSyncSubscriber(
includeHeroes = false,
)
suspend fun subscribe(roomId: RoomId) = mutex.withLock {
withContext(dispatchers.io) {
try {
subscribeToRoom(roomId)
} catch (exception: Exception) {
Timber.e("Failed to subscribe to room $roomId")
}
}
}
suspend fun batchSubscribe(roomIds: List<RoomId>) = mutex.withLock {
withContext(dispatchers.io) {
for (roomId in roomIds) {
suspend fun subscribe(roomId: RoomId) {
mutex.withLock {
withContext(dispatchers.io) {
try {
subscribeToRoom(roomId)
} catch (cancellationException: CancellationException) {
throw cancellationException
if (!isSubscribedTo(roomId)) {
Timber.d("Subscribing to room $roomId}")
roomListService.subscribeToRooms(listOf(roomId.value), settings)
}
subscribedRoomIds.add(roomId)
} catch (exception: Exception) {
Timber.e("Failed to subscribe to room $roomId")
}
@@ -75,14 +67,21 @@ class RoomSyncSubscriber(
}
}
private fun subscribeToRoom(roomId: RoomId) {
if (!isSubscribedTo(roomId)) {
Timber.d("Subscribing to room $roomId}")
roomListService.room(roomId.value).use { roomListItem ->
roomListItem.subscribe(settings)
suspend fun batchSubscribe(roomIds: List<RoomId>) = mutex.withLock {
withContext(dispatchers.io) {
try {
val roomIdsToSubscribeTo = roomIds.filterNot { isSubscribedTo(it) }
if (roomIdsToSubscribeTo.isNotEmpty()) {
Timber.d("Subscribing to rooms: $roomIds")
roomListService.subscribeToRooms(roomIdsToSubscribeTo.map { it.value }, settings)
subscribedRoomIds.addAll(roomIds)
}
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Timber.e(exception, "Failed to subscribe to rooms: $roomIds")
}
}
subscribedRoomIds.add(roomId)
}
fun isSubscribedTo(roomId: RoomId): Boolean {

View File

@@ -192,6 +192,21 @@ class RustMatrixRoom(
}
}
override suspend fun pinnedEventsTimeline(): Result<Timeline> {
return runCatching {
innerRoom.pinnedEventsTimeline(
internalIdPrefix = "pinned_events",
maxEventsToLoad = 100u,
).let { inner ->
createTimeline(inner, isLive = false)
}
}.onFailure {
if (it is CancellationException) {
throw it
}
}
}
override fun destroy() {
roomCoroutineScope.cancel()
liveTimeline.close()
@@ -403,6 +418,12 @@ class RustMatrixRoom(
}
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserPinUnpin(userId.value)
}
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,

View File

@@ -525,6 +525,18 @@ class RustTimeline(
}
}
override suspend fun pinEvent(eventId: EventId): Result<Boolean> = withContext(dispatcher) {
runCatching {
inner.pinEvent(eventId = eventId.value)
}
}
override suspend fun unpinEvent(eventId: EventId): Result<Boolean> = withContext(dispatcher) {
runCatching {
inner.unpinEvent(eventId = eventId.value)
}
}
private suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
return runCatching {
inner.fetchDetailsForEvent(eventId.value)

View File

@@ -47,6 +47,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
eventId = it.eventId()?.let(::EventId),
transactionId = it.transactionId()?.let(::TransactionId),
isEditable = it.isEditable(),
canBeRepliedTo = it.canBeRepliedTo(),
isLocal = it.isLocal(),
isOwn = it.isOwn(),
isRemote = it.isRemote(),

View File

@@ -40,6 +40,7 @@ import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import org.matrix.rustcomponents.sdk.use
import uniffi.matrix_sdk_ui.RoomPinnedEventsChange
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
@@ -176,7 +177,7 @@ private fun RustOtherState.map(): OtherState {
RustOtherState.RoomHistoryVisibility -> OtherState.RoomHistoryVisibility
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
is RustOtherState.RoomName -> OtherState.RoomName(name)
RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents
is RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents(change.map())
is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl
is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName)
@@ -187,6 +188,14 @@ private fun RustOtherState.map(): OtherState {
}
}
private fun RoomPinnedEventsChange.map(): OtherState.RoomPinnedEvents.Change {
return when (this) {
RoomPinnedEventsChange.ADDED -> OtherState.RoomPinnedEvents.Change.ADDED
RoomPinnedEventsChange.REMOVED -> OtherState.RoomPinnedEvents.Change.REMOVED
RoomPinnedEventsChange.CHANGED -> OtherState.RoomPinnedEvents.Change.CHANGED
}
}
private fun RustEncryptedMessage.map(): UnableToDecryptContent.Data {
return when (this) {
is RustEncryptedMessage.MegolmV1AesSha2 -> UnableToDecryptContent.Data.MegolmV1AesSha2(sessionId, cause.map())

View File

@@ -189,6 +189,8 @@ class RoomSummaryListProcessorTest {
override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle {
return TaskHandle(Pointer.NULL)
}
override fun subscribeToRooms(roomIds: List<String>, settings: RoomSubscription?) = Unit
}
}
@@ -221,6 +223,7 @@ private fun aRustRoomInfo(
numUnreadMessages: ULong = 0uL,
numUnreadNotifications: ULong = 0uL,
numUnreadMentions: ULong = 0uL,
pinnedEventIds: List<String> = listOf(),
) = RoomInfo(
id = id,
displayName = displayName,
@@ -249,7 +252,8 @@ private fun aRustRoomInfo(
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions
numUnreadMentions = numUnreadMentions,
pinnedEventIds = pinnedEventIds,
)
class FakeRoomListItem(
@@ -268,6 +272,4 @@ class FakeRoomListItem(
override suspend fun latestEvent(): EventTimelineItem? {
return latestEvent
}
override fun subscribe(settings: RoomSubscription?) = Unit
}