Pinned events : only load data if feature is enabled

This commit is contained in:
ganfra 2024-08-02 11:14:24 +02:00
parent 9985526137
commit 9a8c58313d
3 changed files with 113 additions and 50 deletions

View file

@ -0,0 +1,47 @@
/*
* 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.features.messages.impl.pinned.banner
import androidx.compose.ui.text.AnnotatedString
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.coroutines.withContext
import javax.inject.Inject
class PinnedMessagesBannerItemFactory @Inject constructor(
private val coroutineDispatchers: CoroutineDispatchers,
private val formatter: PinnedMessagesBannerFormatter,
) {
suspend fun create(timelineItem: MatrixTimelineItem): PinnedMessagesBannerItem? = withContext(coroutineDispatchers.computation) {
when (timelineItem) {
is MatrixTimelineItem.Event -> {
val eventId = timelineItem.eventId ?: return@withContext null
val formatted = formatter.format(timelineItem.event)
PinnedMessagesBannerItem(
eventId = eventId,
formatted = if (formatted is AnnotatedString) {
formatted
} else {
AnnotatedString(formatted.toString())
},
)
}
else -> null
}
}
}

View file

@ -18,21 +18,20 @@ package io.element.android.features.messages.impl.pinned.banner
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.text.AnnotatedString
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
@ -42,61 +41,37 @@ import kotlin.time.Duration.Companion.milliseconds
class PinnedMessagesBannerPresenter @Inject constructor( class PinnedMessagesBannerPresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val pinnedMessagesBannerFormatter: PinnedMessagesBannerFormatter, private val itemFactory: PinnedMessagesBannerItemFactory,
private val featureFlagService: FeatureFlagService,
) : Presenter<PinnedMessagesBannerState> { ) : Presenter<PinnedMessagesBannerState> {
@OptIn(FlowPreview::class)
@Composable @Composable
override fun present(): PinnedMessagesBannerState { override fun present(): PinnedMessagesBannerState {
var pinnedMessages by remember { var pinnedItems by remember {
mutableStateOf<List<PinnedMessagesBannerItem>>(emptyList()) mutableStateOf<List<PinnedMessagesBannerItem>>(emptyList())
} }
fun onItemsChange(newItems: List<PinnedMessagesBannerItem>) {
pinnedItems = newItems
}
var currentPinnedMessageIndex by rememberSaveable { var currentPinnedMessageIndex by rememberSaveable {
mutableIntStateOf(0) mutableIntStateOf(0)
} }
LaunchedEffect(pinnedMessages) {
val pinnedMessageCount = pinnedMessages.size LaunchedEffect(pinnedItems) {
val pinnedMessageCount = pinnedItems.size
if (currentPinnedMessageIndex >= pinnedMessageCount) { if (currentPinnedMessageIndex >= pinnedMessageCount) {
currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0) currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0)
} }
} }
LaunchedEffect(Unit) { PinnedMessagesBannerItemsEffect(::onItemsChange)
val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
pinnedEventsTimeline.timelineItems
.debounce(300.milliseconds)
.map { timelineItems ->
timelineItems.mapNotNull { timelineItem ->
when (timelineItem) {
is MatrixTimelineItem.Event -> {
val eventId = timelineItem.eventId ?: return@mapNotNull null
val formatted = pinnedMessagesBannerFormatter.format(timelineItem.event)
PinnedMessagesBannerItem(
eventId = eventId,
formatted = if (formatted is AnnotatedString) {
formatted
} else {
AnnotatedString(formatted.toString())
},
)
}
else -> null
}
}
}
.flowOn(Dispatchers.Default)
.onEach { newPinnedMessages ->
pinnedMessages = newPinnedMessages
}.onCompletion {
pinnedEventsTimeline.close()
}
.launchIn(this)
}
fun handleEvent(event: PinnedMessagesBannerEvents) { fun handleEvent(event: PinnedMessagesBannerEvents) {
when (event) { when (event) {
is PinnedMessagesBannerEvents.MoveToNextPinned -> { is PinnedMessagesBannerEvents.MoveToNextPinned -> {
if (currentPinnedMessageIndex < pinnedMessages.size - 1) { if (currentPinnedMessageIndex < pinnedItems.size - 1) {
currentPinnedMessageIndex++ currentPinnedMessageIndex++
} else { } else {
currentPinnedMessageIndex = 0 currentPinnedMessageIndex = 0
@ -106,10 +81,38 @@ class PinnedMessagesBannerPresenter @Inject constructor(
} }
return PinnedMessagesBannerState( return PinnedMessagesBannerState(
pinnedMessagesCount = pinnedMessages.size, pinnedMessagesCount = pinnedItems.size,
currentPinnedMessage = pinnedMessages.getOrNull(currentPinnedMessageIndex), currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex),
currentPinnedMessageIndex = currentPinnedMessageIndex, currentPinnedMessageIndex = currentPinnedMessageIndex,
eventSink = ::handleEvent eventSink = ::handleEvent
) )
} }
@OptIn(FlowPreview::class)
@Composable
private fun PinnedMessagesBannerItemsEffect(
onItemsChange: (List<PinnedMessagesBannerItem>) -> Unit,
) {
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
LaunchedEffect(isFeatureEnabled) {
if (!isFeatureEnabled) return@LaunchedEffect
val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
pinnedEventsTimeline.timelineItems
.debounce(300.milliseconds)
.map { timelineItems ->
timelineItems.mapNotNull { timelineItem ->
itemFactory.create(timelineItem)
}
}
.onEach { newItems ->
updatedOnItemsChange(newItems)
}.onCompletion {
pinnedEventsTimeline.close()
}
.launchIn(this)
}
}
} }

View file

@ -17,11 +17,14 @@
package io.element.android.features.messages.impl.pinned.banner package io.element.android.features.messages.impl.pinned.banner
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -48,15 +51,25 @@ class PinnedMessagesBannerPresenterTest {
} }
} }
private fun createPinnedMessagesBannerPresenter( private fun TestScope.createPinnedMessagesBannerPresenter(
room: MatrixRoom = FakeMatrixRoom(), room: MatrixRoom = FakeMatrixRoom(),
formatter: PinnedMessagesBannerFormatter = FakePinnedMessagesBannerFormatter( itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory(
formatLambda = { event -> "Content ${event.content}" } coroutineDispatchers = testCoroutineDispatchers(),
) formatter = FakePinnedMessagesBannerFormatter(
formatLambda = { event -> "Content ${event.content}" }
)
),
isFeatureEnabled: Boolean = true,
): PinnedMessagesBannerPresenter { ): PinnedMessagesBannerPresenter {
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.PinnedEvents.key to isFeatureEnabled
)
)
return PinnedMessagesBannerPresenter( return PinnedMessagesBannerPresenter(
room = room, room = room,
pinnedMessagesBannerFormatter = formatter itemFactory = itemFactory,
featureFlagService = featureFlagService
) )
} }
} }