Pinned events : only load data if feature is enabled
This commit is contained in:
parent
f63b59e118
commit
8e8c271bc2
3 changed files with 113 additions and 50 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,21 +18,20 @@ package io.element.android.features.messages.impl.pinned.banner
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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.timeline.MatrixTimelineItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
|
|
@ -42,61 +41,37 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
|
||||
class PinnedMessagesBannerPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val pinnedMessagesBannerFormatter: PinnedMessagesBannerFormatter,
|
||||
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<PinnedMessagesBannerState> {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesBannerState {
|
||||
var pinnedMessages by remember {
|
||||
var pinnedItems by remember {
|
||||
mutableStateOf<List<PinnedMessagesBannerItem>>(emptyList())
|
||||
}
|
||||
|
||||
fun onItemsChange(newItems: List<PinnedMessagesBannerItem>) {
|
||||
pinnedItems = newItems
|
||||
}
|
||||
|
||||
var currentPinnedMessageIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
LaunchedEffect(pinnedMessages) {
|
||||
val pinnedMessageCount = pinnedMessages.size
|
||||
|
||||
LaunchedEffect(pinnedItems) {
|
||||
val pinnedMessageCount = pinnedItems.size
|
||||
if (currentPinnedMessageIndex >= pinnedMessageCount) {
|
||||
currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
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)
|
||||
}
|
||||
PinnedMessagesBannerItemsEffect(::onItemsChange)
|
||||
|
||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||
when (event) {
|
||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||
if (currentPinnedMessageIndex < pinnedMessages.size - 1) {
|
||||
if (currentPinnedMessageIndex < pinnedItems.size - 1) {
|
||||
currentPinnedMessageIndex++
|
||||
} else {
|
||||
currentPinnedMessageIndex = 0
|
||||
|
|
@ -106,10 +81,38 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
return PinnedMessagesBannerState(
|
||||
pinnedMessagesCount = pinnedMessages.size,
|
||||
currentPinnedMessage = pinnedMessages.getOrNull(currentPinnedMessageIndex),
|
||||
pinnedMessagesCount = pinnedItems.size,
|
||||
currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex),
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@
|
|||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
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.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.test.room.FakeMatrixRoom
|
||||
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 org.junit.Test
|
||||
|
||||
|
|
@ -48,15 +51,25 @@ class PinnedMessagesBannerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPinnedMessagesBannerPresenter(
|
||||
private fun TestScope.createPinnedMessagesBannerPresenter(
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
formatter: PinnedMessagesBannerFormatter = FakePinnedMessagesBannerFormatter(
|
||||
formatLambda = { event -> "Content ${event.content}" }
|
||||
)
|
||||
itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory(
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
formatter = FakePinnedMessagesBannerFormatter(
|
||||
formatLambda = { event -> "Content ${event.content}" }
|
||||
)
|
||||
),
|
||||
isFeatureEnabled: Boolean = true,
|
||||
): PinnedMessagesBannerPresenter {
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.PinnedEvents.key to isFeatureEnabled
|
||||
)
|
||||
)
|
||||
return PinnedMessagesBannerPresenter(
|
||||
room = room,
|
||||
pinnedMessagesBannerFormatter = formatter
|
||||
itemFactory = itemFactory,
|
||||
featureFlagService = featureFlagService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue