Timeline : makes sure all tests are passing
This commit is contained in:
parent
9ffed34303
commit
cf1c728eab
12 changed files with 108 additions and 49 deletions
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,22 @@ import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
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.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -49,12 +54,14 @@ class TimelineController @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) : Closeable, TimelineProvider {
|
) : Closeable, TimelineProvider {
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
private val liveTimeline = MutableStateFlow(room.liveTimeline)
|
private val liveTimeline = MutableStateFlow(room.liveTimeline)
|
||||||
private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty())
|
private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty())
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||||
return currentTimelineFlow().flatMapLatest { it.timelineItems }
|
return currentTimelineFlow.flatMapLatest { it.timelineItems }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLive(): Flow<Boolean> {
|
fun isLive(): Flow<Boolean> {
|
||||||
|
|
@ -62,7 +69,7 @@ class TimelineController @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
|
suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
|
||||||
currentTimelineFlow().first().run {
|
currentTimelineFlow.value.run {
|
||||||
block(this)
|
block(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +96,10 @@ class TimelineController @Inject constructor(
|
||||||
* This does close the detached timeline if any.
|
* This does close the detached timeline if any.
|
||||||
*/
|
*/
|
||||||
fun focusOnLive() {
|
fun focusOnLive() {
|
||||||
|
closeDetachedTimeline()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeDetachedTimeline() {
|
||||||
detachedTimeline.getAndUpdate {
|
detachedTimeline.getAndUpdate {
|
||||||
when {
|
when {
|
||||||
it.isPresent -> {
|
it.isPresent -> {
|
||||||
|
|
@ -101,11 +112,12 @@ class TimelineController @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
focusOnLive()
|
coroutineScope.cancel()
|
||||||
|
closeDetachedTimeline()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||||
return currentTimelineFlow().first().paginate(direction)
|
return currentTimelineFlow.value.paginate(direction)
|
||||||
.onSuccess { hasReachedEnd ->
|
.onSuccess { hasReachedEnd ->
|
||||||
if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) {
|
if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) {
|
||||||
focusOnLive()
|
focusOnLive()
|
||||||
|
|
@ -113,14 +125,14 @@ class TimelineController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached ->
|
private val currentTimelineFlow = combine(liveTimeline, detachedTimeline) { live, detached ->
|
||||||
when {
|
when {
|
||||||
detached.isPresent -> detached.get()
|
detached.isPresent -> detached.get()
|
||||||
else -> live
|
else -> live
|
||||||
}
|
}
|
||||||
}
|
}.stateIn(coroutineScope, SharingStarted.Eagerly, room.liveTimeline)
|
||||||
|
|
||||||
override suspend fun getActiveTimeline(): Timeline {
|
override fun activeTimelineFlow(): StateFlow<Timeline> {
|
||||||
return currentTimelineFlow().first()
|
return currentTimelineFlow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ data class TimelineState(
|
||||||
val focusedEventId : EventId?,
|
val focusedEventId : EventId?,
|
||||||
val focusRequestState: FocusRequestState,
|
val focusRequestState: FocusRequestState,
|
||||||
val eventSink: (TimelineEvents) -> Unit,
|
val eventSink: (TimelineEvents) -> Unit,
|
||||||
)
|
){
|
||||||
|
val isTimelineEmpty = timelineItems.none { it is TimelineItem.Event }
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface FocusRequestState {
|
sealed interface FocusRequestState {
|
||||||
data object None : FocusRequestState
|
data object None : FocusRequestState
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
|
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
|
|
@ -125,12 +126,12 @@ fun TimelineView(
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
/*
|
|
||||||
item(key = UUID.randomUUID()) {
|
|
||||||
TypingNotificationView(state = typingNotificationState)
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
if(state.isLive) {
|
||||||
|
item {
|
||||||
|
TypingNotificationView(state = typingNotificationState)
|
||||||
|
}
|
||||||
|
}
|
||||||
items(
|
items(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
contentType = { timelineItem -> timelineItem.contentType() },
|
contentType = { timelineItem -> timelineItem.contentType() },
|
||||||
|
|
@ -165,7 +166,7 @@ fun TimelineView(
|
||||||
)
|
)
|
||||||
|
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
isTimelineEmpty = state.timelineItems.isEmpty(),
|
isTimelineEmpty = state.isTimelineEmpty,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
newEventState = state.newEventState,
|
newEventState = state.newEventState,
|
||||||
|
|
@ -185,10 +186,6 @@ private fun FocusRequestStateView(
|
||||||
onClearFocusRequestState: () -> Unit,
|
onClearFocusRequestState: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) {
|
|
||||||
onClearFocusRequestState()
|
|
||||||
}
|
|
||||||
|
|
||||||
when (focusRequestState) {
|
when (focusRequestState) {
|
||||||
is FocusRequestState.Failure -> {
|
is FocusRequestState.Failure -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
|
@ -198,7 +195,7 @@ private fun FocusRequestStateView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FocusRequestState.Fetching -> {
|
FocusRequestState.Fetching -> {
|
||||||
ProgressDialog(modifier = modifier)
|
ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState)
|
||||||
}
|
}
|
||||||
is FocusRequestState.Cached, FocusRequestState.None -> Unit
|
is FocusRequestState.Cached, FocusRequestState.None -> Unit
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +251,6 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(canAutoScroll, newEventState) {
|
LaunchedEffect(canAutoScroll, newEventState) {
|
||||||
Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState")
|
|
||||||
val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
|
val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
|
||||||
if (shouldScrollToBottom) {
|
if (shouldScrollToBottom) {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,20 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
class ForwardMessagesPresenterTests {
|
class ForwardMessagesPresenterTests {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -37,7 +42,7 @@ class ForwardMessagesPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = aPresenter()
|
val presenter = aForwardMessagesPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -50,7 +55,14 @@ class ForwardMessagesPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - forward successful`() = runTest {
|
fun `present - forward successful`() = runTest {
|
||||||
val presenter = aPresenter()
|
val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val timeline = FakeTimeline().apply {
|
||||||
|
this.forwardEventLambda = forwardEventLambda
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(liveTimeline = timeline)
|
||||||
|
val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -62,18 +74,23 @@ class ForwardMessagesPresenterTests {
|
||||||
val successfulForwardState = awaitItem()
|
val successfulForwardState = awaitItem()
|
||||||
assertThat(successfulForwardState.isForwarding).isFalse()
|
assertThat(successfulForwardState.isForwarding).isFalse()
|
||||||
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
|
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
|
||||||
|
assert(forwardEventLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - select a room and forward failed, then clear`() = runTest {
|
fun `present - select a room and forward failed, then clear`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> ->
|
||||||
val presenter = aPresenter(fakeMatrixRoom = room)
|
Result.failure<Unit>(IllegalStateException("error"))
|
||||||
|
}
|
||||||
|
val timeline = FakeTimeline().apply {
|
||||||
|
this.forwardEventLambda = forwardEventLambda
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(liveTimeline = timeline)
|
||||||
|
val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Test failed forwarding
|
|
||||||
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val summary = aRoomSummaryDetails()
|
val summary = aRoomSummaryDetails()
|
||||||
presenter.onRoomSelected(listOf(summary.roomId))
|
presenter.onRoomSelected(listOf(summary.roomId))
|
||||||
|
|
@ -83,10 +100,11 @@ class ForwardMessagesPresenterTests {
|
||||||
// Then clear error
|
// Then clear error
|
||||||
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
|
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
|
||||||
assertThat(awaitItem().error).isNull()
|
assertThat(awaitItem().error).isNull()
|
||||||
|
assert(forwardEventLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.aPresenter(
|
private fun CoroutineScope.aForwardMessagesPresenter(
|
||||||
eventId: EventId = AN_EVENT_ID,
|
eventId: EventId = AN_EVENT_ID,
|
||||||
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
||||||
coroutineScope: CoroutineScope = this,
|
coroutineScope: CoroutineScope = this,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
|
||||||
|
|
@ -34,22 +34,23 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PollHistoryPresenter @Inject constructor(
|
class PollHistoryPresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
||||||
|
private val timelineProvider: TimelineProvider,
|
||||||
) : Presenter<PollHistoryState> {
|
) : Presenter<PollHistoryState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PollHistoryState {
|
override fun present(): PollHistoryState {
|
||||||
// TODO use room.rememberPollHistory() when working properly?
|
val timeline by timelineProvider.activeTimelineFlow().collectAsState()
|
||||||
val timeline = room.liveTimeline
|
|
||||||
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
|
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
|
||||||
val pollHistoryItemsFlow = remember {
|
val pollHistoryItemsFlow = remember {
|
||||||
timeline.timelineItems.map { items ->
|
timeline.timelineItems.map { items ->
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
|
|
@ -174,11 +175,11 @@ class PollHistoryPresenterTest {
|
||||||
),
|
),
|
||||||
): PollHistoryPresenter {
|
): PollHistoryPresenter {
|
||||||
return PollHistoryPresenter(
|
return PollHistoryPresenter(
|
||||||
room = room,
|
|
||||||
appCoroutineScope = appCoroutineScope,
|
appCoroutineScope = appCoroutineScope,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
pollHistoryItemFactory = pollHistoryItemFactory,
|
pollHistoryItemFactory = pollHistoryItemFactory,
|
||||||
|
timelineProvider = LiveTimelineProvider(room),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ package io.element.android.libraries.matrix.api.timeline
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,9 +31,11 @@ import javax.inject.Inject
|
||||||
* It could be the current room timeline, or a timeline for a specific event.
|
* It could be the current room timeline, or a timeline for a specific event.
|
||||||
*/
|
*/
|
||||||
interface TimelineProvider {
|
interface TimelineProvider {
|
||||||
suspend fun getActiveTimeline(): Timeline
|
fun activeTimelineFlow(): StateFlow<Timeline>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of [TimelineProvider] that provides the live timeline of a room.
|
* Default implementation of [TimelineProvider] that provides the live timeline of a room.
|
||||||
*/
|
*/
|
||||||
|
|
@ -36,6 +43,6 @@ interface TimelineProvider {
|
||||||
class LiveTimelineProvider @Inject constructor(
|
class LiveTimelineProvider @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) : TimelineProvider {
|
) : TimelineProvider {
|
||||||
override suspend fun getActiveTimeline(): Timeline = room.liveTimeline
|
override fun activeTimelineFlow(): StateFlow<Timeline> = MutableStateFlow(room.liveTimeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
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.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
|
internal fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
|
||||||
val firstItem = firstOrNull()
|
val firstItem = firstOrNull()
|
||||||
return firstItem is MatrixTimelineItem.Virtual &&
|
return firstItem is MatrixTimelineItem.Virtual &&
|
||||||
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
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.OtherState
|
||||||
|
|
@ -43,10 +44,7 @@ class RoomBeginningPostProcessor {
|
||||||
|
|
||||||
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||||
if (items.hasEncryptionHistoryBanner()) return items
|
if (items.hasEncryptionHistoryBanner()) return items
|
||||||
val roomBeginningItem = MatrixTimelineItem.Virtual(
|
val roomBeginningItem = createRoomBeginningItem()
|
||||||
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
|
|
||||||
virtual = VirtualTimelineItem.RoomBeginning
|
|
||||||
)
|
|
||||||
return listOf(roomBeginningItem) + items
|
return listOf(roomBeginningItem) + items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,4 +75,13 @@ class RoomBeginningPostProcessor {
|
||||||
}
|
}
|
||||||
return newItems
|
return newItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun createRoomBeginningItem(): MatrixTimelineItem.Virtual {
|
||||||
|
return MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
|
||||||
|
virtual = VirtualTimelineItem.RoomBeginning
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,14 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DmBeginningTimelineProcessorTest {
|
class RoomBeginningPostProcessorTest {
|
||||||
@Test
|
@Test
|
||||||
fun `processor removes room creation event and self-join event from DM timeline`() {
|
fun `processor removes room creation event and self-join event from DM timeline`() {
|
||||||
val timelineItems = listOf(
|
val timelineItems = listOf(
|
||||||
|
|
@ -36,7 +37,7 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEmpty()
|
assertThat(processedItems).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,18 +54,30 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEqualTo(expected)
|
assertThat(processedItems).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `processor won't remove items if it's not a DM`() {
|
fun `processor will add beginning of room item if it's not a DM`() {
|
||||||
val timelineItems = listOf(
|
val timelineItems = listOf(
|
||||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = true)
|
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||||
|
assertThat(processedItems).isEqualTo(
|
||||||
|
listOf(processor.createRoomBeginningItem()) + timelineItems
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() {
|
||||||
|
val timelineItems = listOf(
|
||||||
|
MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner),
|
||||||
|
)
|
||||||
|
val processor = RoomBeginningPostProcessor()
|
||||||
|
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +88,7 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +98,7 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +109,7 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = RoomBeginningPostProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue