Merge pull request #3392 from element-hq/feature/fga/pinned_messages_list
[Feature] Pinned messages list
This commit is contained in:
commit
b802a196fc
98 changed files with 2279 additions and 357 deletions
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
* A [StateFlow] that derives its value from a [Flow].
|
||||
* Useful when you want to apply transformations to a [Flow] and expose it as a [StateFlow].
|
||||
*/
|
||||
class DerivedStateFlow<T>(
|
||||
private val getValue: () -> T,
|
||||
private val flow: Flow<T>
|
||||
) : StateFlow<T> {
|
||||
override val replayCache: List<T>
|
||||
get() = listOf(value)
|
||||
|
||||
override val value: T
|
||||
get() = getValue()
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
||||
coroutineScope { flow.distinctUntilChanged().stateIn(this).collect(collector) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the value of a [StateFlow] to a new value and returns a new [StateFlow] with the mapped value.
|
||||
*/
|
||||
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
|
||||
return DerivedStateFlow(
|
||||
getValue = { transform(this.value) },
|
||||
flow = this.map { a -> transform(a) }
|
||||
)
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
api(projects.libraries.sessionStorage.api)
|
||||
implementation(libs.coroutines.core)
|
||||
api(projects.libraries.architecture)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@ interface Timeline : AutoCloseable {
|
|||
FORWARDS
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
LIVE,
|
||||
FOCUSED_ON_EVENT,
|
||||
PINNED_EVENTS
|
||||
}
|
||||
|
||||
val membershipChangeEventReceived: Flow<Unit>
|
||||
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
|
||||
|
|
|
|||
|
|
@ -17,14 +17,16 @@
|
|||
package io.element.android.libraries.matrix.api.timeline
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* This interface defines a way to get the active timeline.
|
||||
* It could be the current room timeline, or a timeline for a specific event.
|
||||
* It could be the live timeline, a pinned timeline or a detached timeline.
|
||||
* By default, the active timeline is the live timeline.
|
||||
*/
|
||||
interface TimelineProvider {
|
||||
fun activeTimelineFlow(): StateFlow<Timeline>
|
||||
fun activeTimelineFlow(): StateFlow<Timeline?>
|
||||
}
|
||||
|
||||
suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first()
|
||||
suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().filterNotNull().first()
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class RustMatrixRoom(
|
|||
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||
|
||||
override val liveTimeline = createTimeline(innerTimeline, isLive = true) {
|
||||
override val liveTimeline = createTimeline(innerTimeline, mode = Timeline.Mode.LIVE) {
|
||||
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ class RustMatrixRoom(
|
|||
numContextEvents = 50u,
|
||||
internalIdPrefix = "focus_$eventId",
|
||||
).let { inner ->
|
||||
createTimeline(inner, isLive = false)
|
||||
createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_EVENT)
|
||||
}
|
||||
}.mapFailure {
|
||||
it.toFocusEventException()
|
||||
|
|
@ -199,7 +199,7 @@ class RustMatrixRoom(
|
|||
internalIdPrefix = "pinned_events",
|
||||
maxEventsToLoad = 100u,
|
||||
).let { inner ->
|
||||
createTimeline(inner, isLive = false)
|
||||
createTimeline(inner, mode = Timeline.Mode.PINNED_EVENTS)
|
||||
}
|
||||
}.onFailure {
|
||||
if (it is CancellationException) {
|
||||
|
|
@ -656,13 +656,13 @@ class RustMatrixRoom(
|
|||
|
||||
private fun createTimeline(
|
||||
timeline: InnerTimeline,
|
||||
isLive: Boolean,
|
||||
mode: Timeline.Mode,
|
||||
onNewSyncedEvent: () -> Unit = {},
|
||||
): Timeline {
|
||||
val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline")
|
||||
return RustTimeline(
|
||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||
isLive = isLive,
|
||||
mode = mode,
|
||||
matrixRoom = this,
|
||||
systemClock = systemClock,
|
||||
coroutineScope = timelineCoroutineScope,
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ private const val PAGINATION_SIZE = 50
|
|||
|
||||
class RustTimeline(
|
||||
private val inner: InnerTimeline,
|
||||
private val isLive: Boolean,
|
||||
mode: Timeline.Mode,
|
||||
systemClock: SystemClock,
|
||||
isKeyBackupEnabled: Boolean,
|
||||
private val matrixRoom: MatrixRoom,
|
||||
|
|
@ -132,21 +132,21 @@ class RustTimeline(
|
|||
onNewSyncedEvent = onNewSyncedEvent,
|
||||
)
|
||||
|
||||
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
|
||||
private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode)
|
||||
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(isLive)
|
||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||
|
||||
private val backPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
||||
)
|
||||
|
||||
private val forwardPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = !isLive)
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
|
||||
)
|
||||
|
||||
init {
|
||||
coroutineScope.fetchMembers()
|
||||
if (isLive) {
|
||||
if (mode == Timeline.Mode.LIVE) {
|
||||
// When timeline is live, we need to listen to the back pagination status as
|
||||
// sdk can automatically paginate backwards.
|
||||
coroutineScope.registerBackPaginationStatusListener()
|
||||
|
|
|
|||
|
|
@ -18,21 +18,22 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
||||
/**
|
||||
* This post processor is responsible for adding virtual items to indicate all the previous last forward item.
|
||||
*/
|
||||
class LastForwardIndicatorsPostProcessor(
|
||||
private val isTimelineLive: Boolean,
|
||||
private val mode: Timeline.Mode,
|
||||
) {
|
||||
private val lastForwardIdentifiers = LinkedHashSet<UniqueId>()
|
||||
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
): List<MatrixTimelineItem> {
|
||||
// If the timeline is live, we don't have any last forward indicator to display
|
||||
if (isTimelineLive) {
|
||||
// We don't need to add the last forward indicator if we are not in the FOCUSED_ON_EVENT mode
|
||||
if (mode != Timeline.Mode.FOCUSED_ON_EVENT) {
|
||||
return items
|
||||
} else {
|
||||
return buildList {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
|
@ -29,13 +30,14 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
|||
* This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs
|
||||
* or add the RoomBeginning item for non DM room.
|
||||
*/
|
||||
class RoomBeginningPostProcessor {
|
||||
class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
isDm: Boolean,
|
||||
hasMoreToLoadBackwards: Boolean
|
||||
): List<MatrixTimelineItem> {
|
||||
return when {
|
||||
mode == Timeline.Mode.PINNED_EVENTS -> items
|
||||
hasMoreToLoadBackwards -> items
|
||||
isDm -> processForDM(items)
|
||||
else -> processForRoom(items)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
|
@ -37,7 +38,7 @@ class RoomBeginningPostProcessorTest {
|
|||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEmpty()
|
||||
}
|
||||
|
|
@ -60,7 +61,7 @@ class RoomBeginningPostProcessorTest {
|
|||
),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEqualTo(expected)
|
||||
}
|
||||
|
|
@ -71,7 +72,7 @@ class RoomBeginningPostProcessorTest {
|
|||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEqualTo(
|
||||
listOf(processor.createRoomBeginningItem()) + timelineItems
|
||||
|
|
@ -83,7 +84,7 @@ class RoomBeginningPostProcessorTest {
|
|||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Virtual(UniqueId("EncryptedHistoryBanner"), VirtualTimelineItem.EncryptedHistoryBanner),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
|
@ -94,7 +95,7 @@ class RoomBeginningPostProcessorTest {
|
|||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
|
@ -104,7 +105,7 @@ class RoomBeginningPostProcessorTest {
|
|||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
|
@ -118,7 +119,7 @@ class RoomBeginningPostProcessorTest {
|
|||
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
|
||||
),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor()
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue