Pinned events : only load data if feature is enabled
This commit is contained in:
parent
9985526137
commit
9a8c58313d
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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue