From 8e8c271bc20db7926cbc8b880f2aee17092c0fc8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Aug 2024 11:14:24 +0200 Subject: [PATCH] Pinned events : only load data if feature is enabled --- .../banner/PinnedMessagesBannerItemFactory.kt | 47 ++++++++++ .../banner/PinnedMessagesBannerPresenter.kt | 91 ++++++++++--------- .../PinnedMessagesBannerPresenterTest.kt | 25 +++-- 3 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt new file mode 100644 index 0000000000..95c13e1f64 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt @@ -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 + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index d5968b6a7b..bc594ff49c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -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 { - @OptIn(FlowPreview::class) @Composable override fun present(): PinnedMessagesBannerState { - var pinnedMessages by remember { + var pinnedItems by remember { mutableStateOf>(emptyList()) } + + fun onItemsChange(newItems: List) { + 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) -> 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) + } + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index 9ed181d9e1..baab17577d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -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 ) } }