Merge branch 'develop' into feature/fga/better_timeline_scroll
This commit is contained in:
commit
8f01e8133f
156 changed files with 3806 additions and 539 deletions
|
|
@ -157,6 +157,7 @@ class RustMatrixClient constructor(
|
|||
coroutineDispatchers = dispatchers,
|
||||
systemClock = clock,
|
||||
roomContentForwarder = roomContentForwarder,
|
||||
sessionData = sessionStore.getSession(sessionId.value)!!,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue