Merge branch 'develop' into feature/fga/better_media_handling

This commit is contained in:
ganfra 2023-07-18 15:13:59 +02:00
commit 84500d41eb
221 changed files with 4574 additions and 845 deletions

View file

@ -157,6 +157,7 @@ class RustMatrixClient constructor(
coroutineDispatchers = dispatchers,
systemClock = clock,
roomContentForwarder = roomContentForwarder,
sessionData = sessionStore.getSession(sessionId.value)!!,
)
}
@ -164,8 +165,10 @@ class RustMatrixClient constructor(
val cachedRoomListItem = roomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom()
if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId")
null
} else {
Timber.d("Found room cached for $roomId")
Pair(cachedRoomListItem, fullRoom)
}
}

View file

@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
fun Throwable.mapAuthenticationException(): Throwable {
fun Throwable.mapAuthenticationException(): AuthenticationException {
return when (this) {
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!)
is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!)
@ -35,6 +35,6 @@ fun Throwable.mapAuthenticationException(): Throwable {
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
*/
else -> this
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
}
}

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.RustMatrixClient
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -45,6 +46,7 @@ import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.use
import java.io.File
import java.util.Date
import javax.inject.Inject
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
@ -94,7 +96,7 @@ class RustMatrixAuthenticationService @Inject constructor(
throw IllegalStateException("No session to restore with id $sessionId")
}
}.mapFailure { failure ->
failure.mapAuthenticationException()
failure.mapClientException()
}
}
@ -208,4 +210,5 @@ private fun Session.toSessionData() = SessionData(
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
slidingSyncProxy = slidingSyncProxy,
loginTimestamp = Date(),
)

View file

@ -0,0 +1,27 @@
/*
* 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.libraries.matrix.impl.exception
import io.element.android.libraries.matrix.api.exception.ClientException
import org.matrix.rustcomponents.sdk.ClientException as RustClientException
fun Throwable.mapClientException(): ClientException {
return when (this) {
is RustClientException.Generic -> ClientException.Generic(msg)
else -> ClientException.Other(message ?: "Unknown error")
}
}

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@ -41,6 +42,8 @@ import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
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.ExperimentalCoroutinesApi
@ -72,6 +75,7 @@ class RustMatrixRoom(
private val coroutineDispatchers: CoroutineDispatchers,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val sessionData: SessionData,
) : MatrixRoom {
override val roomId = RoomId(innerRoom.id())
@ -90,7 +94,8 @@ class RustMatrixRoom(
matrixRoom = this,
innerRoom = innerRoom,
roomCoroutineScope = roomCoroutineScope,
dispatcher = roomDispatcher
dispatcher = roomDispatcher,
lastLoginTimestamp = sessionData.loginTimestamp,
)
}
@ -218,10 +223,10 @@ class RustMatrixRoom(
}
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit> = withContext(roomDispatcher) {
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result<Unit> = withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId)
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId?.value)
}
} else {
runCatching {
@ -326,17 +331,17 @@ class RustMatrixRoom(
}
}
override suspend fun retrySendMessage(transactionId: String): Result<Unit> =
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> =
withContext(roomDispatcher) {
runCatching {
innerRoom.retrySend(transactionId)
innerRoom.retrySend(transactionId.value)
}
}
override suspend fun cancelSend(transactionId: String): Result<Unit> =
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
withContext(roomDispatcher) {
runCatching {
innerRoom.cancelSend(transactionId)
innerRoom.cancelSend(transactionId.value)
}
}

View file

@ -21,19 +21,23 @@ 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
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import kotlinx.coroutines.CompletableDeferred
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackPaginationStatus
@ -43,6 +47,7 @@ import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.Date
private const val INITIAL_MAX_SIZE = 50
@ -51,6 +56,7 @@ class RustMatrixTimeline(
private val matrixRoom: MatrixRoom,
private val innerRoom: Room,
private val dispatcher: CoroutineDispatcher,
private val lastLoginTimestamp: Date?,
) : MatrixTimeline {
private val initLatch = CompletableDeferred<Unit>()
@ -63,6 +69,12 @@ class RustMatrixTimeline(
MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false)
)
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = matrixRoom.isEncrypted,
paginationStateFlow = _paginationState,
)
private val timelineItemFactory = MatrixTimelineItemMapper(
fetchDetailsForEvent = this::fetchDetailsForEvent,
roomCoroutineScope = roomCoroutineScope,
@ -81,8 +93,11 @@ class RustMatrixTimeline(
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
@OptIn(FlowPreview::class)
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.sample(50)
.mapLatest { items ->
encryptedHistoryPostProcessor.process(items)
}
internal suspend fun postItems(items: List<TimelineItem>) {
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
@ -100,6 +115,12 @@ class RustMatrixTimeline(
internal fun postPaginationStatus(status: BackPaginationStatus) {
_paginationState.getAndUpdate { currentPaginationState ->
if (hasEncryptionHistoryBanner()) {
return@getAndUpdate currentPaginationState.copy(
isBackPaginating = false,
hasMoreToLoadBackwards = false,
)
}
when (status) {
BackPaginationStatus.IDLE -> {
currentPaginationState.copy(
@ -159,4 +180,10 @@ class RustMatrixTimeline(
fun getItemById(eventId: EventId): MatrixTimelineItem.Event? {
return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event
}
private fun hasEncryptionHistoryBanner(): Boolean {
val firstItem = _timelineItems.value.firstOrNull()
return firstItem is MatrixTimelineItem.Virtual
&& firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
}
}

View file

@ -17,13 +17,16 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
@ -34,7 +37,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
EventTimelineItem(
eventId = it.eventId()?.let(::EventId),
transactionId = it.transactionId(),
transactionId = it.transactionId()?.let(::TransactionId),
isEditable = it.isEditable(),
isLocal = it.isLocal(),
isOwn = it.isOwn(),
@ -46,6 +49,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
timestamp = it.timestamp().toLong(),
content = contentMapper.map(it.content()),
debugInfo = it.debugInfo().map(),
origin = it.origin()?.map()
)
}
}
@ -90,3 +94,11 @@ private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo {
latestEditedJson = latestEditJson,
)
}
private fun RustEventItemOrigin.map(): TimelineItemEventOrigin {
return when (this) {
RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL
RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC
RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION
}
}

View file

@ -0,0 +1,74 @@
/*
* 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.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import java.util.Date
import java.util.UUID
class TimelineEncryptedHistoryPostProcessor(
private val lastLoginTimestamp: Date?,
private val isRoomEncrypted: Boolean,
private val paginationStateFlow: MutableStateFlow<MatrixTimeline.PaginationState>,
) {
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
if (!isRoomEncrypted || lastLoginTimestamp == null) return items
val filteredItems = replaceWithEncryptionHistoryBannerIfNeeded(items)
// Disable back pagination
val wasFiltered = filteredItems !== items
if (wasFiltered) {
paginationStateFlow.getAndUpdate {
it.copy(
isBackPaginating = false,
hasMoreToLoadBackwards = false
)
}
}
return filteredItems
}
private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
var lastEncryptedHistoryBannerIndex = -1
for ((i, item) in list.withIndex()) {
if (isItemEncryptionHistory(item)) {
lastEncryptedHistoryBannerIndex = i
}
}
return if (lastEncryptedHistoryBannerIndex >= 0) {
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
sublist.add(0, MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner))
sublist
} else {
list
}
}
private fun isItemEncryptionHistory(item: MatrixTimelineItem): Boolean {
if ((item as? MatrixTimelineItem.Virtual)?.virtual is VirtualTimelineItem.EncryptedHistoryBanner) {
return true
}
val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false
return timestamp <= lastLoginTimestamp!!.time
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Test
import java.util.Date
class TimelineEncryptedHistoryPostProcessorTest {
private val defaultLastLoginTimestamp = Date(1689061264L)
@Test
fun `given an unencrypted room, nothing is done`() {
val processor = createPostProcessor(isRoomEncrypted = false)
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a null lastLoginTimestamp, nothing is done`() {
val processor = createPostProcessor(lastLoginTimestamp = null)
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given an empty list, nothing is done`() {
val processor = createPostProcessor()
val items = emptyList<MatrixTimelineItem>()
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a list with no items before lastLoginTimestamp, nothing is done`() {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
)
assertThat(processor.process(items))
.isEqualTo(listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner)))
}
@Test
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
)
assertThat(processor.process(items)).isEqualTo(
listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner))
)
}
@Test
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, they're replaced and the user can't back paginate`() {
val paginationStateFlow = MutableStateFlow(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false))
val processor = createPostProcessor(paginationStateFlow = paginationStateFlow)
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
)
assertThat(processor.process(items)).isEqualTo(
listOf(
MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner),
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
)
assertThat(paginationStateFlow.value).isEqualTo(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = false, isBackPaginating = false))
}
private fun createPostProcessor(
lastLoginTimestamp: Date? = defaultLastLoginTimestamp,
isRoomEncrypted: Boolean = true,
paginationStateFlow: MutableStateFlow<MatrixTimeline.PaginationState> =
MutableStateFlow(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false))
) = TimelineEncryptedHistoryPostProcessor(
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = isRoomEncrypted,
paginationStateFlow = paginationStateFlow,
)
}