PollHistory : simplify so we only have one Node. Also enrich PollHistoryState.

This commit is contained in:
ganfra 2023-12-06 19:27:50 +01:00
parent 67ec58a4f4
commit 79d59eb5de
19 changed files with 376 additions and 255 deletions

View file

@ -20,6 +20,16 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList 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( data class PollContentState(
val eventId: EventId?, val eventId: EventId?,
val question: String, val question: String,

View file

@ -17,6 +17,8 @@
package io.element.android.features.poll.api.pollcontent 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.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
fun aPollQuestion() = "What type of food should we have at the party?" fun aPollQuestion() = "What type of food should we have at the party?"
@ -79,3 +81,25 @@ fun aPollAnswerItem(
votesCount = votesCount, votesCount = votesCount,
percentage = percentage 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<PollAnswerItem> = aPollAnswerItemList(
isEnded = isEnded,
isDisclosed = isDisclosed,
hasVotes = hasVotes
),
) = PollContentState(
eventId = null,
question = question,
answerItems = answerItems,
pollKind = pollKind,
isPollEditable = isMine && !isEnded,
isPollEnded = isEnded,
isMine = isMine,
)

View file

@ -16,6 +16,7 @@
package io.element.android.features.poll.impl.history 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 import io.element.android.libraries.matrix.api.core.EventId
sealed interface PollHistoryEvents { sealed interface PollHistoryEvents {
@ -23,4 +24,5 @@ sealed interface PollHistoryEvents {
data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents
data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents
data object EditPoll : PollHistoryEvents data object EditPoll : PollHistoryEvents
data class OnFilterSelected(val filter: PollHistoryFilter) : PollHistoryEvents
} }

View file

@ -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<Plugin>,
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,
)
}
}

View file

@ -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<Plugin>,
) : Node(
buildContext = buildContext,
plugins = plugins
) {
@Composable
override fun View(modifier: Modifier) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
}

View file

@ -16,96 +16,32 @@
package io.element.android.features.poll.impl.history package io.element.android.features.poll.impl.history
import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin 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.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode 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.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) @ContributesNode(RoomScope::class)
class PollHistoryNode @AssistedInject constructor( class PollHistoryNode @AssistedInject constructor(
private val room: MatrixRoom,
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
) : BackstackNode<PollHistoryNode.NavTarget>( private val presenter: PollHistoryPresenter,
backstack = BackStack( ) : Node(
initialElement = NavTarget.PollHistoryLoading,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, 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<PollHistoryLoadingNode>(
buildContext = buildContext,
)
is NavTarget.PollHistoryLoaded -> {
createNode<PollHistoryLoadedNode>(
buildContext = buildContext,
plugins = listOf(
PollHistoryLoadedNode.Inputs(
pollHistory = pollHistory ?: error("Poll history not loaded"),
)
),
)
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
Children( PollHistoryView(
navModel = backstack, state = presenter.present(),
modifier = modifier, modifier = modifier,
transitionHandler = rememberDefaultTransitionHandler(), goBack = this::navigateUp,
) )
} }
} }

View file

@ -19,53 +19,62 @@ package io.element.android.features.poll.impl.history
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.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted import androidx.compose.runtime.saveable.rememberSaveable
import dagger.assisted.AssistedFactory import androidx.compose.runtime.setValue
import dagger.assisted.AssistedInject
import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction 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.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline 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.CoroutineScope
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class PollHistoryPresenter @AssistedInject constructor( class PollHistoryPresenter @Inject constructor(
@Assisted private val pollHistory: MatrixTimeline, private val room: MatrixRoom,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val sendPollResponseAction: SendPollResponseAction, private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction, private val endPollAction: EndPollAction,
private val pollHistoryItemFactory: PollHistoryItemsFactory, private val pollHistoryItemFactory: PollHistoryItemsFactory,
) : Presenter<PollHistoryState> { ) : Presenter<PollHistoryState> {
@AssistedFactory
interface Factory {
fun create(
pollHistory: MatrixTimeline,
): PollHistoryPresenter
}
@Composable @Composable
override fun present(): PollHistoryState { override fun present(): PollHistoryState {
val pollHistory = room.rememberPollHistory()
val paginationState by pollHistory.paginationState.collectAsState() val paginationState by pollHistory.paginationState.collectAsState()
val timelineItemsFlow = remember { val pollHistoryItemsFlow = remember {
pollHistory.timelineItems.map { items -> pollHistory.timelineItems.map { items ->
pollHistoryItemFactory.create(items) pollHistoryItemFactory.create(items)
} }
} }
val items by timelineItemsFlow.collectAsState(initial = emptyList()) var activeFilter by rememberSaveable {
LaunchedEffect(items.size) { mutableStateOf(PollHistoryFilter.ONGOING)
if (items.isEmpty()) loadMore() }
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() val coroutineScope = rememberCoroutineScope()
fun handleEvents(event: PollHistoryEvents) { fun handleEvents(event: PollHistoryEvents) {
when (event) { when (event) {
is PollHistoryEvents.LoadMore -> { is PollHistoryEvents.LoadMore -> {
coroutineScope.loadMore() coroutineScope.loadMore(pollHistory)
} }
is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch { is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch {
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId) sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
@ -74,16 +83,31 @@ class PollHistoryPresenter @AssistedInject constructor(
endPollAction.execute(pollStartId = event.pollStartId) endPollAction.execute(pollStartId = event.pollStartId)
} }
PollHistoryEvents.EditPoll -> Unit 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( return PollHistoryState(
paginationState = paginationState, isLoading = isLoading,
pollItems = items.toImmutableList(), hasMoreToLoad = paginationState.hasMoreToLoadBackwards,
currentItems = currentItems,
activeFilter = activeFilter,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun CoroutineScope.loadMore() = launch { private fun CoroutineScope.loadMore(pollHistory: MatrixTimeline) = launch {
pollHistory.paginateBackwards(20, 3) Timber.d("LoadMore poll history")
pollHistory.paginateBackwards(50, 3)
} }
} }

View file

@ -16,11 +16,14 @@
package io.element.android.features.poll.impl.history 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 import kotlinx.collections.immutable.ImmutableList
data class PollHistoryState( data class PollHistoryState(
val paginationState: MatrixTimeline.PaginationState, val isLoading: Boolean,
val pollItems: ImmutableList<PollHistoryItem>, val hasMoreToLoad: Boolean,
val activeFilter: PollHistoryFilter,
val currentItems: ImmutableList<PollHistoryItem>,
val eventSink: (PollHistoryEvents) -> Unit, val eventSink: (PollHistoryEvents) -> Unit,
) )

View file

@ -17,8 +17,48 @@
package io.element.android.features.poll.impl.history package io.element.android.features.poll.impl.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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<PollHistoryState> { class PollHistoryStateProvider : PreviewParameterProvider<PollHistoryState> {
override val values: Sequence<PollHistoryState> override val values: Sequence<PollHistoryState>
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<PollHistoryItem> = 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,
)

View file

@ -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,
}

View file

@ -14,10 +14,11 @@
* limitations under the License. * 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 import io.element.android.features.poll.api.pollcontent.PollContentState
sealed interface PollHistoryItem { data class PollHistoryItem(
data class PollContent(val formattedDate: String, val state: PollContentState) : PollHistoryItem val formattedDate: String,
} val state: PollContentState,
)

View file

@ -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<PollHistoryItem> = persistentListOf(),
val past: ImmutableList<PollHistoryItem> = persistentListOf(),
) {
val size = ongoing.size + past.size
}

View file

@ -14,13 +14,14 @@
* limitations under the License. * 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.features.poll.api.pollcontent.PollContentStateFactory
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter 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.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -30,8 +31,22 @@ class PollHistoryItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) { ) {
suspend fun create(timelineItems: List<MatrixTimelineItem>): List<PollHistoryItem> = withContext(dispatchers.computation) { suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) {
timelineItems.mapNotNull { create(it) }.reversed() val past = ArrayList<PollHistoryItem>()
val ongoing = ArrayList<PollHistoryItem>()
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? { private suspend fun create(timelineItem: MatrixTimelineItem): PollHistoryItem? {
@ -39,7 +54,7 @@ class PollHistoryItemsFactory @Inject constructor(
is MatrixTimelineItem.Event -> { is MatrixTimelineItem.Event -> {
val pollContent = timelineItem.event.content as? PollContent ?: return null val pollContent = timelineItem.event.content as? PollContent ?: return null
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent) val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
PollHistoryItem.PollContent( PollHistoryItem(
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp), formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
state = pollContentState state = pollContentState
) )

View file

@ -240,7 +240,7 @@ interface MatrixRoom : Closeable {
*/ */
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
suspend fun pollHistory(): MatrixTimeline fun pollHistory(): MatrixTimeline
override fun close() = destroy() override fun close() = destroy()
} }

View file

@ -23,9 +23,9 @@ import kotlinx.coroutines.flow.StateFlow
interface MatrixTimeline : AutoCloseable { interface MatrixTimeline : AutoCloseable {
data class PaginationState( data class PaginationState(
val isBackPaginating: Boolean, val isBackPaginating: Boolean = false,
val hasMoreToLoadBackwards: Boolean, val hasMoreToLoadBackwards: Boolean = true,
val beginningOfRoomReached: Boolean, val beginningOfRoomReached: Boolean = false,
) { ) {
val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards
} }

View file

@ -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.location.AssetType
import io.element.android.libraries.matrix.api.room.roomMembers 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.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.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.toProgressWatcher 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.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.poll.toInner 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.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.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll import io.element.android.libraries.matrix.impl.util.destroyAll
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow 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.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.Timeline
import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilities
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
@ -85,14 +85,16 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import org.matrix.rustcomponents.sdk.Room as InnerRoom
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
private val isKeyBackupEnabled: Boolean, private val isKeyBackupEnabled: Boolean,
private val roomListItem: RoomListItem, private val roomListItem: RoomListItem,
private val innerRoom: Room, private val innerRoom: InnerRoom,
private val innerTimeline: Timeline, private val innerTimeline: InnerTimeline,
private val roomNotificationSettingsService: RustNotificationSettingsService, private val roomNotificationSettingsService: RustNotificationSettingsService,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
@ -130,15 +132,9 @@ class RustMatrixRoom(
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown) private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
override val timeline = RustMatrixTimeline( override val timeline = createMatrixTimeline(innerTimeline) {
isKeyBackupEnabled = isKeyBackupEnabled, _syncUpdateFlow.value = systemClock.epochMillis()
matrixRoom = this, }
innerTimeline = innerTimeline,
roomCoroutineScope = roomCoroutineScope,
dispatcher = roomDispatcher,
lastLoginTimestamp = sessionData.loginTimestamp,
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
)
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow() override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow()
@ -150,7 +146,7 @@ class RustMatrixRoom(
override fun destroy() { override fun destroy() {
roomCoroutineScope.cancel() roomCoroutineScope.cancel()
innerTimeline.destroy() timeline.close()
innerRoom.destroy() innerRoom.destroy()
roomListItem.destroy() roomListItem.destroy()
specialModeEventTimelineItem?.destroy() specialModeEventTimelineItem?.destroy()
@ -570,22 +566,35 @@ class RustMatrixRoom(
) )
} }
override suspend fun pollHistory() = RustMatrixTimeline( override fun pollHistory() = AsyncMatrixTimeline(
isKeyBackupEnabled = isKeyBackupEnabled, coroutineScope = roomCoroutineScope,
matrixRoom = this, dispatcher = roomDispatcher
innerTimeline = innerRoom.pollHistory(), ) {
roomCoroutineScope = roomCoroutineScope, val innerTimeline = innerRoom.pollHistory()
dispatcher = roomDispatcher, createMatrixTimeline(innerTimeline)
lastLoginTimestamp = sessionData.loginTimestamp, }
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
)
private suspend fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> { private fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
return runCatching { return runCatching {
MediaUploadHandlerImpl(files, handle()) 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 = private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
if (htmlBody != null) { if (htmlBody != null) {
messageEventContentFromHtml(body, htmlBody) messageEventContentFromHtml(body, htmlBody)

View file

@ -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<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
private val _paginationState = MutableStateFlow(
MatrixTimeline.PaginationState()
)
private val timeline = coroutineScope.async(context = dispatcher, start = CoroutineStart.LAZY) {
timelineProvider()
}
private val closeSignal = CompletableDeferred<Unit>()
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<MatrixTimeline.PaginationState> = _paginationState
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
return timeline.await().paginateBackwards(requestSize, untilNumberOfItems)
}
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
return timeline.await().fetchDetailsForEvent(eventId)
}
override suspend fun sendReadReceipt(eventId: EventId): Result<Unit> {
return timeline.await().sendReadReceipt(eventId)
}
override fun close() {
closeSignal.complete(Unit)
}
}

View file

@ -72,11 +72,7 @@ class RustMatrixTimeline(
MutableStateFlow(emptyList()) MutableStateFlow(emptyList())
private val _paginationState = MutableStateFlow( private val _paginationState = MutableStateFlow(
MatrixTimeline.PaginationState( MatrixTimeline.PaginationState()
hasMoreToLoadBackwards = true,
isBackPaginating = false,
beginningOfRoomReached = false,
)
) )
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(

View file

@ -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
}