Merge pull request #3432 from element-hq/feature/fga/pinned_messages_fix_timeline_provider
Feature/fga/pinned messages fix timeline provider
This commit is contained in:
commit
2960631126
3 changed files with 46 additions and 13 deletions
|
|
@ -18,11 +18,13 @@ 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 io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -32,7 +34,8 @@ class PinnedEventsTimelineProvider @Inject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : TimelineProvider {
|
) : TimelineProvider {
|
||||||
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> = MutableStateFlow(AsyncData.Uninitialized)
|
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> =
|
||||||
|
MutableStateFlow(AsyncData.Uninitialized)
|
||||||
|
|
||||||
override fun activeTimelineFlow(): StateFlow<Timeline?> {
|
override fun activeTimelineFlow(): StateFlow<Timeline?> {
|
||||||
return _timelineStateFlow
|
return _timelineStateFlow
|
||||||
|
|
@ -44,25 +47,46 @@ class PinnedEventsTimelineProvider @Inject constructor(
|
||||||
val timelineStateFlow = _timelineStateFlow
|
val timelineStateFlow = _timelineStateFlow
|
||||||
|
|
||||||
fun launchIn(scope: CoroutineScope) {
|
fun launchIn(scope: CoroutineScope) {
|
||||||
|
_timelineStateFlow.subscriptionCount
|
||||||
|
.map { count -> count > 0 }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { isActive ->
|
||||||
|
if (isActive) {
|
||||||
|
onActive()
|
||||||
|
} else {
|
||||||
|
onInactive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onActive() = coroutineScope {
|
||||||
combine(
|
combine(
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents),
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents),
|
||||||
networkMonitor.connectivity
|
networkMonitor.connectivity
|
||||||
) {
|
) { isEnabled, _ ->
|
||||||
// do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed
|
// do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed
|
||||||
isEnabled, _ ->
|
|
||||||
isEnabled
|
isEnabled
|
||||||
}
|
}
|
||||||
.onEach { isFeatureEnabled ->
|
.onEach { isFeatureEnabled ->
|
||||||
if (isFeatureEnabled) {
|
if (isFeatureEnabled) {
|
||||||
loadTimelineIfNeeded()
|
loadTimelineIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
_timelineStateFlow.value = AsyncData.Uninitialized
|
resetTimeline()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onCompletion {
|
.launchIn(this)
|
||||||
invokeOnTimeline { close() }
|
}
|
||||||
}
|
|
||||||
.launchIn(scope)
|
private suspend fun onInactive() {
|
||||||
|
resetTimeline()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resetTimeline() {
|
||||||
|
invokeOnTimeline {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
_timelineStateFlow.emit(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) {
|
suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -59,6 +58,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
private val timelineProvider: PinnedEventsTimelineProvider,
|
private val timelineProvider: PinnedEventsTimelineProvider,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<PinnedMessagesListState> {
|
) : Presenter<PinnedMessagesListState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -93,10 +93,9 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
fun handleEvents(event: PinnedMessagesListEvents) {
|
fun handleEvents(event: PinnedMessagesListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PinnedMessagesListEvents.HandleAction -> coroutineScope.handleTimelineAction(event.action, event.event)
|
is PinnedMessagesListEvents.HandleAction -> appCoroutineScope.handleTimelineAction(event.action, event.event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,14 @@ import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class PinnedMessagesListPresenterTest {
|
class PinnedMessagesListPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state feature disabled`() = runTest {
|
fun `present - initial state feature disabled`() = runTest {
|
||||||
|
|
@ -155,6 +158,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||||
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
assert(redactEventLambda)
|
assert(redactEventLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
|
|
@ -184,9 +188,11 @@ class PinnedMessagesListPresenterTest {
|
||||||
|
|
||||||
pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda
|
pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda
|
pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|
||||||
|
|
@ -221,6 +227,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||||
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
assert(onViewInTimelineClickLambda)
|
assert(onViewInTimelineClickLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
|
|
@ -249,6 +256,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||||
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
assert(onShowEventDebugInfoClickLambda)
|
assert(onShowEventDebugInfoClickLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
|
|
@ -277,6 +285,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||||
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
val eventItem = filledState.timelineItems.first() as TimelineItem.Event
|
||||||
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem))
|
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem))
|
||||||
|
advanceUntilIdle()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
assert(onForwardEventClickLambda)
|
assert(onForwardEventClickLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
|
|
@ -322,6 +331,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
timelineProvider = timelineProvider,
|
timelineProvider = timelineProvider,
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
||||||
|
appCoroutineScope = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue