From aa9693126f1e42660bbb1118e092e59b736d9f3d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 6 Dec 2023 19:27:50 +0100 Subject: [PATCH] PollHistory : simplify so we only have one Node. Also enrich PollHistoryState. --- .../poll/api/pollcontent/PollContentState.kt | 10 ++ ...rovider.kt => PollContentStateFixtures.kt} | 24 +++++ .../poll/impl/history/PollHistoryEvents.kt | 2 + .../impl/history/PollHistoryLoadedNode.kt | 69 -------------- .../impl/history/PollHistoryLoadingNode.kt | 50 ---------- .../poll/impl/history/PollHistoryNode.kt | 74 +-------------- .../poll/impl/history/PollHistoryPresenter.kt | 68 ++++++++----- .../poll/impl/history/PollHistoryState.kt | 9 +- .../impl/history/PollHistoryStateProvider.kt | 42 +++++++- .../impl/history/model/PollHistoryFilter.kt | 22 +++++ .../history/{ => model}/PollHistoryItem.kt | 9 +- .../impl/history/model/PollHistoryItems.kt | 27 ++++++ .../{ => model}/PollHistoryItemsFactory.kt | 23 ++++- .../libraries/matrix/api/room/MatrixRoom.kt | 2 +- .../matrix/api/timeline/MatrixTimeline.kt | 6 +- .../matrix/impl/room/RustMatrixRoom.kt | 57 ++++++----- .../impl/timeline/AsyncMatrixTimeline.kt | 95 +++++++++++++++++++ .../impl/timeline/RustMatrixTimeline.kt | 6 +- .../libraries/matrix/ui/room/PollHistory.kt | 36 +++++++ 19 files changed, 376 insertions(+), 255 deletions(-) rename features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/{PollAnswerViewProvider.kt => PollContentStateFixtures.kt} (78%) delete mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadedNode.kt delete mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadingNode.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryFilter.kt rename features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/{ => model}/PollHistoryItem.kt (77%) create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItems.kt rename features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/{ => model}/PollHistoryItemsFactory.kt (70%) create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/PollHistory.kt diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt index 0051f7ebd3..b97029a22c 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt @@ -20,6 +20,16 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind import kotlinx.collections.immutable.ImmutableList +/** + * UI model for a PollContent. + * @property eventId the event id of the poll. + * @property question the poll question. + * @property answerItems the list of answers. + * @property pollKind the kind of poll. + * @property isPollEditable whether the poll is editable. + * @property isPollEnded whether the poll is ended. + * @property isMine whether the poll has been created by me. + */ data class PollContentState( val eventId: EventId?, val question: String, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt similarity index 78% rename from features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerViewProvider.kt rename to features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt index 206acf02c6..80c6a6a6c4 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerViewProvider.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt @@ -17,6 +17,8 @@ package io.element.android.features.poll.api.pollcontent import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf fun aPollQuestion() = "What type of food should we have at the party?" @@ -79,3 +81,25 @@ fun aPollAnswerItem( votesCount = votesCount, percentage = percentage ) + +fun aPollContentState( + isMine: Boolean = false, + isEnded: Boolean = false, + isDisclosed: Boolean = true, + hasVotes: Boolean = true, + question: String = aPollQuestion(), + pollKind: PollKind = PollKind.Disclosed, + answerItems: ImmutableList = aPollAnswerItemList( + isEnded = isEnded, + isDisclosed = isDisclosed, + hasVotes = hasVotes + ), +) = PollContentState( + eventId = null, + question = question, + answerItems = answerItems, + pollKind = pollKind, + isPollEditable = isMine && !isEnded, + isPollEnded = isEnded, + isMine = isMine, +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt index e13f9020b7..d07b6fa24a 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt @@ -16,6 +16,7 @@ package io.element.android.features.poll.impl.history +import io.element.android.features.poll.impl.history.model.PollHistoryFilter import io.element.android.libraries.matrix.api.core.EventId sealed interface PollHistoryEvents { @@ -23,4 +24,5 @@ sealed interface PollHistoryEvents { data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents data object EditPoll : PollHistoryEvents + data class OnFilterSelected(val filter: PollHistoryFilter) : PollHistoryEvents } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadedNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadedNode.kt deleted file mode 100644 index 665700e52c..0000000000 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadedNode.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.poll.impl.history - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.lifecycle.subscribe -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import io.element.android.services.analytics.api.AnalyticsService - -@ContributesNode(RoomScope::class) -class PollHistoryLoadedNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: PollHistoryPresenter.Factory, - analyticsService: AnalyticsService, -) : Node( - buildContext = buildContext, - plugins = plugins -) { - data class Inputs( - val pollHistory: MatrixTimeline, - ) : NodeInputs - - private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create( - inputs.pollHistory, - ) - - init { - lifecycle.subscribe( - onResume = { - // analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView)) // TODO - } - ) - } - - @Composable - override fun View(modifier: Modifier) { - PollHistoryView( - state = presenter.present(), - modifier = modifier, - goBack = this::navigateUp, - ) - } -} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadingNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadingNode.kt deleted file mode 100644 index 4eddd009e3..0000000000 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryLoadingNode.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.poll.impl.history - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.di.RoomScope - -@ContributesNode(RoomScope::class) -class PollHistoryLoadingNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, -) : Node( - buildContext = buildContext, - plugins = plugins -) { - @Composable - override fun View(modifier: Modifier) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } -} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt index 2ae20c8e12..d466a16aef 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryNode.kt @@ -16,96 +16,32 @@ package io.element.android.features.poll.impl.history -import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope -import com.bumble.appyx.core.composable.Children -import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.BackstackNode -import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler -import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) class PollHistoryNode @AssistedInject constructor( - private val room: MatrixRoom, @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( - backstack = BackStack( - initialElement = NavTarget.PollHistoryLoading, - savedStateMap = buildContext.savedStateMap, - ), + private val presenter: PollHistoryPresenter, +) : Node( buildContext = buildContext, plugins = plugins, ) { - sealed interface NavTarget : Parcelable { - @Parcelize - data object PollHistoryLoading : NavTarget - - @Parcelize - data object PollHistoryLoaded : NavTarget - } - - private var pollHistory: MatrixTimeline? = null - - override fun onBuilt() { - super.onBuilt() - lifecycle.subscribe( - onCreate = { - lifecycleScope.launch { - runCatching { - room.pollHistory() - }.onSuccess { - pollHistory = it - backstack.newRoot(NavTarget.PollHistoryLoaded) - } - } - }, - onDestroy = { - pollHistory?.close() - }, - ) - } - - override fun resolve( - navTarget: NavTarget, - buildContext: BuildContext - ): Node = when (navTarget) { - is NavTarget.PollHistoryLoading -> createNode( - buildContext = buildContext, - ) - is NavTarget.PollHistoryLoaded -> { - createNode( - buildContext = buildContext, - plugins = listOf( - PollHistoryLoadedNode.Inputs( - pollHistory = pollHistory ?: error("Poll history not loaded"), - ) - ), - ) - } - } @Composable override fun View(modifier: Modifier) { - Children( - navModel = backstack, + PollHistoryView( + state = presenter.present(), modifier = modifier, - transitionHandler = rememberDefaultTransitionHandler(), + goBack = this::navigateUp, ) } } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index 40db94306b..a1f0b81315 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -19,53 +19,62 @@ package io.element.android.features.poll.impl.history import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction +import io.element.android.features.poll.impl.history.model.PollHistoryFilter +import io.element.android.features.poll.impl.history.model.PollHistoryItems +import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import kotlinx.collections.immutable.toImmutableList +import io.element.android.libraries.matrix.ui.room.rememberPollHistory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject -class PollHistoryPresenter @AssistedInject constructor( - @Assisted private val pollHistory: MatrixTimeline, +class PollHistoryPresenter @Inject constructor( + private val room: MatrixRoom, private val appCoroutineScope: CoroutineScope, private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, private val pollHistoryItemFactory: PollHistoryItemsFactory, ) : Presenter { - @AssistedFactory - interface Factory { - fun create( - pollHistory: MatrixTimeline, - ): PollHistoryPresenter - } - @Composable override fun present(): PollHistoryState { + val pollHistory = room.rememberPollHistory() val paginationState by pollHistory.paginationState.collectAsState() - val timelineItemsFlow = remember { + val pollHistoryItemsFlow = remember { pollHistory.timelineItems.map { items -> pollHistoryItemFactory.create(items) } } - val items by timelineItemsFlow.collectAsState(initial = emptyList()) - LaunchedEffect(items.size) { - if (items.isEmpty()) loadMore() + var activeFilter by rememberSaveable { + mutableStateOf(PollHistoryFilter.ONGOING) + } + val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems()) + LaunchedEffect(paginationState, pollHistoryItems.size) { + if (pollHistoryItems.size == 0 && paginationState.canBackPaginate) loadMore(pollHistory) + } + val isLoading by remember { + derivedStateOf { + pollHistoryItems.size == 0 || paginationState.isBackPaginating + } } val coroutineScope = rememberCoroutineScope() fun handleEvents(event: PollHistoryEvents) { when (event) { is PollHistoryEvents.LoadMore -> { - coroutineScope.loadMore() + coroutineScope.loadMore(pollHistory) } is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch { sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId) @@ -74,16 +83,31 @@ class PollHistoryPresenter @AssistedInject constructor( endPollAction.execute(pollStartId = event.pollStartId) } PollHistoryEvents.EditPoll -> Unit + is PollHistoryEvents.OnFilterSelected -> { + activeFilter = event.filter + } + } + } + + val currentItems by remember { + derivedStateOf { + when (activeFilter) { + PollHistoryFilter.ONGOING -> pollHistoryItems.ongoing + PollHistoryFilter.PAST -> pollHistoryItems.past + } } } return PollHistoryState( - paginationState = paginationState, - pollItems = items.toImmutableList(), + isLoading = isLoading, + hasMoreToLoad = paginationState.hasMoreToLoadBackwards, + currentItems = currentItems, + activeFilter = activeFilter, eventSink = ::handleEvents, ) } - private fun CoroutineScope.loadMore() = launch { - pollHistory.paginateBackwards(20, 3) + private fun CoroutineScope.loadMore(pollHistory: MatrixTimeline) = launch { + Timber.d("LoadMore poll history") + pollHistory.paginateBackwards(50, 3) } } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt index 1c636060ec..c3b768993f 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt @@ -16,11 +16,14 @@ package io.element.android.features.poll.impl.history -import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.features.poll.impl.history.model.PollHistoryFilter +import io.element.android.features.poll.impl.history.model.PollHistoryItem import kotlinx.collections.immutable.ImmutableList data class PollHistoryState( - val paginationState: MatrixTimeline.PaginationState, - val pollItems: ImmutableList, + val isLoading: Boolean, + val hasMoreToLoad: Boolean, + val activeFilter: PollHistoryFilter, + val currentItems: ImmutableList, val eventSink: (PollHistoryEvents) -> Unit, ) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt index 880b68d178..24862cd599 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt @@ -17,8 +17,48 @@ package io.element.android.features.poll.impl.history import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.poll.api.pollcontent.PollContentState +import io.element.android.features.poll.api.pollcontent.aPollContentState +import io.element.android.features.poll.impl.history.model.PollHistoryFilter +import io.element.android.features.poll.impl.history.model.PollHistoryItem +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf class PollHistoryStateProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf() // TODO + get() = sequenceOf( + aPollHistoryState( + isLoading = false, + hasMoreToLoad = false, + activeFilter = PollHistoryFilter.ONGOING, + ), + aPollHistoryState( + isLoading = true, + hasMoreToLoad = true, + activeFilter = PollHistoryFilter.PAST, + ), + ) } + +private fun aPollHistoryState( + isLoading: Boolean = false, + hasMoreToLoad: Boolean = false, + activeFilter: PollHistoryFilter = PollHistoryFilter.ONGOING, + currentItems: ImmutableList = persistentListOf( + aPollHistoryItem(), + ), +) = PollHistoryState( + isLoading = isLoading, + hasMoreToLoad = hasMoreToLoad, + activeFilter = activeFilter, + currentItems = currentItems, + eventSink = {}, +) + +private fun aPollHistoryItem( + formattedDate: String = "01/12/2023", + state: PollContentState = aPollContentState(), +) = PollHistoryItem( + formattedDate = formattedDate, + state = state, +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryFilter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryFilter.kt new file mode 100644 index 0000000000..d56087d2c1 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryFilter.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.poll.impl.history.model + +enum class PollHistoryFilter { + ONGOING, + PAST, +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItem.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItem.kt similarity index 77% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItem.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItem.kt index 5b12dc8b2c..6b55b249bb 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItem.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItem.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.element.android.features.poll.impl.history +package io.element.android.features.poll.impl.history.model import io.element.android.features.poll.api.pollcontent.PollContentState -sealed interface PollHistoryItem { - data class PollContent(val formattedDate: String, val state: PollContentState) : PollHistoryItem -} +data class PollHistoryItem( + val formattedDate: String, + val state: PollContentState, +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItems.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItems.kt new file mode 100644 index 0000000000..180e31b458 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItems.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.poll.impl.history.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class PollHistoryItems( + val ongoing: ImmutableList = persistentListOf(), + val past: ImmutableList = persistentListOf(), +) { + val size = ongoing.size + past.size +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItemsFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt similarity index 70% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItemsFactory.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt index 1d09e115ae..a85e4cada2 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItemsFactory.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package io.element.android.features.poll.impl.history +package io.element.android.features.poll.impl.history.model import io.element.android.features.poll.api.pollcontent.PollContentStateFactory import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.withContext import javax.inject.Inject @@ -30,8 +31,22 @@ class PollHistoryItemsFactory @Inject constructor( private val dispatchers: CoroutineDispatchers, ) { - suspend fun create(timelineItems: List): List = withContext(dispatchers.computation) { - timelineItems.mapNotNull { create(it) }.reversed() + suspend fun create(timelineItems: List): PollHistoryItems = withContext(dispatchers.computation) { + val past = ArrayList() + val ongoing = ArrayList() + for (index in timelineItems.indices.reversed()) { + val timelineItem = timelineItems[index] + val pollHistoryItem = create(timelineItem) ?: continue + if (pollHistoryItem.state.isPollEnded) { + past.add(pollHistoryItem) + } else { + ongoing.add(pollHistoryItem) + } + } + PollHistoryItems( + ongoing = ongoing.toPersistentList(), + past = past.toPersistentList() + ) } private suspend fun create(timelineItem: MatrixTimelineItem): PollHistoryItem? { @@ -39,7 +54,7 @@ class PollHistoryItemsFactory @Inject constructor( is MatrixTimelineItem.Event -> { val pollContent = timelineItem.event.content as? PollContent ?: return null val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent) - PollHistoryItem.PollContent( + PollHistoryItem( formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp), state = pollContentState ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index d7c8d7f49c..a7cbdccfa2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -240,7 +240,7 @@ interface MatrixRoom : Closeable { */ fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result - suspend fun pollHistory(): MatrixTimeline + fun pollHistory(): MatrixTimeline override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt index 98d5772d23..f2a81ea32e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.flow.StateFlow interface MatrixTimeline : AutoCloseable { data class PaginationState( - val isBackPaginating: Boolean, - val hasMoreToLoadBackwards: Boolean, - val beginningOfRoomReached: Boolean, + val isBackPaginating: Boolean = false, + val hasMoreToLoadBackwards: Boolean = true, + val beginningOfRoomReached: Boolean = false, ) { val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8ec2d2b7b8..e509d05ad2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.toProgressWatcher @@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner +import io.element.android.libraries.matrix.impl.timeline.AsyncMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll import io.element.android.libraries.matrix.impl.util.mxCallbackFlow @@ -70,14 +72,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle -import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml @@ -85,14 +85,16 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File +import org.matrix.rustcomponents.sdk.Room as InnerRoom +import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @OptIn(ExperimentalCoroutinesApi::class) class RustMatrixRoom( override val sessionId: SessionId, private val isKeyBackupEnabled: Boolean, private val roomListItem: RoomListItem, - private val innerRoom: Room, - private val innerTimeline: Timeline, + private val innerRoom: InnerRoom, + private val innerTimeline: InnerTimeline, private val roomNotificationSettingsService: RustNotificationSettingsService, sessionCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, @@ -130,15 +132,9 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val timeline = RustMatrixTimeline( - isKeyBackupEnabled = isKeyBackupEnabled, - matrixRoom = this, - innerTimeline = innerTimeline, - roomCoroutineScope = roomCoroutineScope, - dispatcher = roomDispatcher, - lastLoginTimestamp = sessionData.loginTimestamp, - onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() } - ) + override val timeline = createMatrixTimeline(innerTimeline) { + _syncUpdateFlow.value = systemClock.epochMillis() + } override val membersStateFlow: StateFlow = _membersStateFlow.asStateFlow() @@ -150,7 +146,7 @@ class RustMatrixRoom( override fun destroy() { roomCoroutineScope.cancel() - innerTimeline.destroy() + timeline.close() innerRoom.destroy() roomListItem.destroy() specialModeEventTimelineItem?.destroy() @@ -570,22 +566,35 @@ class RustMatrixRoom( ) } - override suspend fun pollHistory() = RustMatrixTimeline( - isKeyBackupEnabled = isKeyBackupEnabled, - matrixRoom = this, - innerTimeline = innerRoom.pollHistory(), - roomCoroutineScope = roomCoroutineScope, - dispatcher = roomDispatcher, - lastLoginTimestamp = sessionData.loginTimestamp, - onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() } - ) + override fun pollHistory() = AsyncMatrixTimeline( + coroutineScope = roomCoroutineScope, + dispatcher = roomDispatcher + ) { + val innerTimeline = innerRoom.pollHistory() + createMatrixTimeline(innerTimeline) + } - private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) } } + private fun createMatrixTimeline( + timeline: InnerTimeline, + onNewSyncedEvent: () -> Unit = {}, + ): MatrixTimeline { + return RustMatrixTimeline( + isKeyBackupEnabled = isKeyBackupEnabled, + matrixRoom = this, + roomCoroutineScope = roomCoroutineScope, + dispatcher = roomDispatcher, + lastLoginTimestamp = sessionData.loginTimestamp, + onNewSyncedEvent = onNewSyncedEvent, + innerTimeline = timeline, + ) + } + private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = if (htmlBody != null) { messageEventContentFromHtml(body, htmlBody) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt new file mode 100644 index 0000000000..15959476b1 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.libraries.matrix.impl.timeline + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * This class is a wrapper around a [MatrixTimeline] that will be created asynchronously. + */ +class AsyncMatrixTimeline( + coroutineScope: CoroutineScope, + dispatcher: CoroutineDispatcher, + private val timelineProvider: suspend () -> MatrixTimeline +) : MatrixTimeline { + + private val _timelineItems: MutableStateFlow> = + MutableStateFlow(emptyList()) + + private val _paginationState = MutableStateFlow( + MatrixTimeline.PaginationState() + ) + private val timeline = coroutineScope.async(context = dispatcher, start = CoroutineStart.LAZY) { + timelineProvider() + } + private val closeSignal = CompletableDeferred() + + init { + coroutineScope.launch { + val delegateTimeline = timeline.await() + delegateTimeline.timelineItems + .onEach { _timelineItems.value = it } + .launchIn(this) + delegateTimeline.paginationState + .onEach { _paginationState.value = it } + .launchIn(this) + + launch { + withContext(NonCancellable) { + closeSignal.await() + Timber.d("Close delegate") + delegateTimeline.close() + } + } + } + } + + override val paginationState: StateFlow = _paginationState + override val timelineItems: Flow> = _timelineItems + + override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result { + return timeline.await().paginateBackwards(requestSize, untilNumberOfItems) + } + + override suspend fun fetchDetailsForEvent(eventId: EventId): Result { + return timeline.await().fetchDetailsForEvent(eventId) + } + + override suspend fun sendReadReceipt(eventId: EventId): Result { + return timeline.await().sendReadReceipt(eventId) + } + + override fun close() { + closeSignal.complete(Unit) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 70978c4fe7..244b73c247 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -72,11 +72,7 @@ class RustMatrixTimeline( MutableStateFlow(emptyList()) private val _paginationState = MutableStateFlow( - MatrixTimeline.PaginationState( - hasMoreToLoadBackwards = true, - isBackPaginating = false, - beginningOfRoomReached = false, - ) + MatrixTimeline.PaginationState() ) private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/PollHistory.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/PollHistory.kt new file mode 100644 index 0000000000..bc695864f7 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/PollHistory.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.libraries.matrix.ui.room + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline + +@Composable +fun MatrixRoom.rememberPollHistory(): MatrixTimeline { + val pollHistory = remember { + pollHistory() + } + DisposableEffect(pollHistory) { + onDispose { + pollHistory.close() + } + } + return pollHistory +}