Merge branch 'develop' into feature/fga/better_timeline_scroll

This commit is contained in:
ganfra 2023-07-17 23:35:41 +02:00
commit 8f01e8133f
156 changed files with 3806 additions and 539 deletions

View file

@ -157,6 +157,7 @@ class RustMatrixClient constructor(
coroutineDispatchers = dispatchers,
systemClock = clock,
roomContentForwarder = roomContentForwarder,
sessionData = sessionStore.getSession(sessionId.value)!!,
)
}

View file

@ -45,6 +45,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
@ -208,4 +209,5 @@ private fun Session.toSessionData() = SessionData(
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
slidingSyncProxy = slidingSyncProxy,
loginTimestamp = Date(),
)

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,6 +17,7 @@
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
@ -36,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(),

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,
)
}