Pinned events : try sharing pinned events timeline instance
This commit is contained in:
parent
060b0350e0
commit
33ed44c789
9 changed files with 352 additions and 131 deletions
|
|
@ -41,6 +41,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
||||||
import io.element.android.features.messages.impl.forward.ForwardMessagesNode
|
import io.element.android.features.messages.impl.forward.ForwardMessagesNode
|
||||||
|
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode
|
import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode
|
||||||
import io.element.android.features.messages.impl.report.ReportMessageNode
|
import io.element.android.features.messages.impl.report.ReportMessageNode
|
||||||
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
||||||
|
|
@ -96,6 +97,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
||||||
private val mentionSpanTheme: MentionSpanTheme,
|
private val mentionSpanTheme: MentionSpanTheme,
|
||||||
|
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Messages,
|
initialElement = NavTarget.Messages,
|
||||||
|
|
@ -163,6 +165,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
roomMemberProfilesCache.replace(membersState.joinedRoomMembers())
|
roomMemberProfilesCache.replace(membersState.joinedRoomMembers())
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
pinnedEventsTimelineProvider.launchIn(lifecycleScope)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
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.Timeline
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
class PinnedEventsTimelineProvider @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
) {
|
||||||
|
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> = MutableStateFlow(AsyncData.Uninitialized)
|
||||||
|
|
||||||
|
val timelineStateFlow = _timelineStateFlow
|
||||||
|
|
||||||
|
fun launchIn(scope: CoroutineScope) {
|
||||||
|
combine(
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents),
|
||||||
|
networkMonitor.connectivity
|
||||||
|
) { isEnabled, _ -> isEnabled }
|
||||||
|
.onEach { isFeatureEnabled ->
|
||||||
|
if (isFeatureEnabled) {
|
||||||
|
loadTimelineIfNeeded()
|
||||||
|
} else {
|
||||||
|
_timelineStateFlow.value = AsyncData.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
invokeOnTimeline { it.close() }
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun invokeOnTimeline(action: suspend (Timeline) -> Unit) {
|
||||||
|
when (val asyncTimeline = timelineStateFlow.value) {
|
||||||
|
is AsyncData.Success -> action(asyncTimeline.data)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadTimelineIfNeeded() {
|
||||||
|
when (timelineStateFlow.value) {
|
||||||
|
is AsyncData.Uninitialized, is AsyncData.Failure -> {
|
||||||
|
timelineStateFlow.emit(AsyncData.Loading())
|
||||||
|
room.pinnedEventsTimeline()
|
||||||
|
.fold(
|
||||||
|
{ timelineStateFlow.emit(AsyncData.Success(it)) },
|
||||||
|
{ timelineStateFlow.emit(AsyncData.Failure(it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,18 +26,19 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
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 io.element.android.features.messages.impl.pinned.IsPinnedMessagesFeatureEnabled
|
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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.room.MatrixRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
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.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
@ -45,46 +46,38 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
class PinnedMessagesBannerPresenter @Inject constructor(
|
class PinnedMessagesBannerPresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val itemFactory: PinnedMessagesBannerItemFactory,
|
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||||
private val isFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
private val timelineController: PinnedEventsTimelineProvider,
|
||||||
private val networkMonitor: NetworkMonitor,
|
|
||||||
) : Presenter<PinnedMessagesBannerState> {
|
) : Presenter<PinnedMessagesBannerState> {
|
||||||
private val pinnedItems = mutableStateOf<ImmutableList<PinnedMessagesBannerItem>>(persistentListOf())
|
private val pinnedItems = mutableStateOf<AsyncData<ImmutableList<PinnedMessagesBannerItem>>>(AsyncData.Uninitialized)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinnedMessagesBannerState {
|
override fun present(): PinnedMessagesBannerState {
|
||||||
val isFeatureEnabled = isFeatureEnabled()
|
|
||||||
val expectedPinnedMessagesCount by remember {
|
val expectedPinnedMessagesCount by remember {
|
||||||
room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size }
|
room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size }
|
||||||
}.collectAsState(initial = 0)
|
}.collectAsState(initial = 0)
|
||||||
|
|
||||||
var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) }
|
var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
PinnedMessagesBannerItemsEffect(
|
PinnedMessagesBannerItemsEffect(
|
||||||
isFeatureEnabled = isFeatureEnabled,
|
|
||||||
onItemsChange = { newItems ->
|
onItemsChange = { newItems ->
|
||||||
val pinnedMessageCount = newItems.size
|
val pinnedMessageCount = newItems.dataOrNull().orEmpty().size
|
||||||
if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) {
|
if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) {
|
||||||
currentPinnedMessageIndex = pinnedMessageCount - 1
|
currentPinnedMessageIndex = pinnedMessageCount - 1
|
||||||
}
|
}
|
||||||
pinnedItems.value = newItems
|
pinnedItems.value = newItems
|
||||||
},
|
},
|
||||||
onTimelineFail = { hasTimelineFailed ->
|
|
||||||
hasTimelineFailedToLoad = hasTimelineFailed
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||||
currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(pinnedItems.value.size)
|
val loadedCount = pinnedItems.value.dataOrNull().orEmpty().size
|
||||||
|
currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(loadedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pinnedMessagesBannerState(
|
return pinnedMessagesBannerState(
|
||||||
isFeatureEnabled = isFeatureEnabled,
|
|
||||||
hasTimelineFailed = hasTimelineFailedToLoad,
|
|
||||||
expectedPinnedMessagesCount = expectedPinnedMessagesCount,
|
expectedPinnedMessagesCount = expectedPinnedMessagesCount,
|
||||||
pinnedItems = pinnedItems.value,
|
pinnedItems = pinnedItems.value,
|
||||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
|
@ -94,63 +87,65 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun pinnedMessagesBannerState(
|
private fun pinnedMessagesBannerState(
|
||||||
isFeatureEnabled: Boolean,
|
|
||||||
hasTimelineFailed: Boolean,
|
|
||||||
expectedPinnedMessagesCount: Int,
|
expectedPinnedMessagesCount: Int,
|
||||||
pinnedItems: ImmutableList<PinnedMessagesBannerItem>,
|
pinnedItems: AsyncData<ImmutableList<PinnedMessagesBannerItem>>,
|
||||||
currentPinnedMessageIndex: Int,
|
currentPinnedMessageIndex: Int,
|
||||||
eventSink: (PinnedMessagesBannerEvents) -> Unit
|
eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
): PinnedMessagesBannerState {
|
): PinnedMessagesBannerState {
|
||||||
val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex)
|
return when (pinnedItems) {
|
||||||
return when {
|
is AsyncData.Failure, is AsyncData.Uninitialized -> PinnedMessagesBannerState.Hidden
|
||||||
!isFeatureEnabled -> PinnedMessagesBannerState.Hidden
|
is AsyncData.Loading -> {
|
||||||
hasTimelineFailed -> PinnedMessagesBannerState.Hidden
|
if (expectedPinnedMessagesCount == 0) {
|
||||||
currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded(
|
PinnedMessagesBannerState.Hidden
|
||||||
currentPinnedMessage = currentPinnedMessage,
|
} else {
|
||||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount)
|
||||||
loadedPinnedMessagesCount = pinnedItems.size,
|
}
|
||||||
eventSink = eventSink
|
}
|
||||||
)
|
is AsyncData.Success -> {
|
||||||
expectedPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden
|
val currentPinnedMessage = pinnedItems.data.getOrNull(currentPinnedMessageIndex)
|
||||||
else -> PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount)
|
if (currentPinnedMessage == null) {
|
||||||
|
PinnedMessagesBannerState.Hidden
|
||||||
|
} else {
|
||||||
|
PinnedMessagesBannerState.Loaded(
|
||||||
|
loadedPinnedMessagesCount = pinnedItems.data.size,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
currentPinnedMessage = currentPinnedMessage,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinnedMessagesBannerItemsEffect(
|
private fun PinnedMessagesBannerItemsEffect(
|
||||||
isFeatureEnabled: Boolean,
|
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit,
|
||||||
onItemsChange: (ImmutableList<PinnedMessagesBannerItem>) -> Unit,
|
|
||||||
onTimelineFail: (Boolean) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||||
val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail)
|
LaunchedEffect(Unit) {
|
||||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
timelineController.timelineStateFlow
|
||||||
|
.flatMapLatest { asyncTimeline ->
|
||||||
|
when (asyncTimeline) {
|
||||||
|
AsyncData.Uninitialized -> flowOf(AsyncData.Uninitialized)
|
||||||
|
is AsyncData.Failure -> flowOf(AsyncData.Failure(asyncTimeline.error))
|
||||||
|
is AsyncData.Loading -> flowOf(AsyncData.Loading())
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
asyncTimeline.data.timelineItems
|
||||||
|
.debounce(300.milliseconds)
|
||||||
|
.map { timelineItems ->
|
||||||
|
val pinnedItems = timelineItems.mapNotNull { timelineItem ->
|
||||||
|
itemFactory.create(timelineItem)
|
||||||
|
}.toImmutableList()
|
||||||
|
|
||||||
LaunchedEffect(isFeatureEnabled, networkStatus) {
|
AsyncData.Success(pinnedItems)
|
||||||
if (!isFeatureEnabled) {
|
}
|
||||||
updatedOnItemsChange(persistentListOf())
|
}
|
||||||
return@LaunchedEffect
|
}
|
||||||
}
|
|
||||||
val pinnedEventsTimeline = room.pinnedEventsTimeline()
|
|
||||||
.onFailure { updatedOnTimelineFail(true) }
|
|
||||||
.onSuccess { updatedOnTimelineFail(false) }
|
|
||||||
.getOrNull()
|
|
||||||
?: return@LaunchedEffect
|
|
||||||
|
|
||||||
pinnedEventsTimeline.timelineItems
|
|
||||||
.debounce(300.milliseconds)
|
|
||||||
.map { timelineItems ->
|
|
||||||
timelineItems.mapNotNull { timelineItem ->
|
|
||||||
itemFactory.create(timelineItem)
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
}
|
||||||
.onEach { newItems ->
|
.onEach { newItems ->
|
||||||
updatedOnItemsChange(newItems)
|
updatedOnItemsChange(newItems)
|
||||||
}
|
}
|
||||||
.onCompletion {
|
|
||||||
pinnedEventsTimeline.close()
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,53 +18,118 @@ package io.element.android.features.messages.impl.pinned.list
|
||||||
|
|
||||||
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.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PinnedMessagesListPresenter @Inject constructor(
|
class PinnedMessagesListPresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
|
private val timelineProvider: PinnedEventsTimelineProvider,
|
||||||
) : Presenter<PinnedMessagesListState> {
|
) : Presenter<PinnedMessagesListState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinnedMessagesListState {
|
override fun present(): PinnedMessagesListState {
|
||||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
|
||||||
val timelineRoomInfo = remember {
|
val timelineRoomInfo = remember {
|
||||||
TimelineRoomInfo(
|
TimelineRoomInfo(
|
||||||
isDm = room.isDm,
|
isDm = room.isDm,
|
||||||
name = room.displayName,
|
name = room.displayName,
|
||||||
|
// We don't need to compute those values
|
||||||
userHasPermissionToSendMessage = false,
|
userHasPermissionToSendMessage = false,
|
||||||
userHasPermissionToSendReaction = false,
|
userHasPermissionToSendReaction = false,
|
||||||
isCallOngoing = false,
|
isCallOngoing = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
|
var pinnedMessageItems by remember {
|
||||||
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
|
mutableStateOf<AsyncData<ImmutableList<TimelineItem>>>(AsyncData.Uninitialized)
|
||||||
timelineItemsFactory.replaceWith(
|
|
||||||
timelineItems = items,
|
|
||||||
roomMembers = membersState.roomMembers().orEmpty()
|
|
||||||
)
|
|
||||||
items
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PinnedMessagesListEffect(
|
||||||
|
onItemsChange = { newItems ->
|
||||||
|
pinnedMessageItems = newItems
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
fun handleEvents(event: PinnedMessagesListEvents) {
|
fun handleEvents(event: PinnedMessagesListEvents) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return PinnedMessagesListState(
|
return pinnedMessagesListState(
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
timelineItems = timelineItems,
|
timelineItems = pinnedMessageItems,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
|
||||||
|
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||||
|
|
||||||
|
val timelineState by timelineProvider.timelineStateFlow.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(timelineState) {
|
||||||
|
when (val asyncTimeline = timelineState) {
|
||||||
|
AsyncData.Uninitialized -> flowOf(AsyncData.Uninitialized)
|
||||||
|
is AsyncData.Failure -> flowOf(AsyncData.Failure(asyncTimeline.error))
|
||||||
|
is AsyncData.Loading -> flowOf(AsyncData.Loading())
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
combine(asyncTimeline.data.timelineItems, room.membersStateFlow) { items, membersState ->
|
||||||
|
timelineItemsFactory.replaceWith(
|
||||||
|
timelineItems = items,
|
||||||
|
roomMembers = membersState.roomMembers().orEmpty()
|
||||||
|
)
|
||||||
|
}.launchIn(this)
|
||||||
|
|
||||||
|
timelineItemsFactory.timelineItems.map { timelineItems ->
|
||||||
|
AsyncData.Success(timelineItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEach { items ->
|
||||||
|
updatedOnItemsChange(items)
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pinnedMessagesListState(
|
||||||
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
|
timelineItems: AsyncData<ImmutableList<TimelineItem>>,
|
||||||
|
eventSink: (PinnedMessagesListEvents) -> Unit
|
||||||
|
): PinnedMessagesListState {
|
||||||
|
return when (timelineItems) {
|
||||||
|
AsyncData.Uninitialized, is AsyncData.Loading -> PinnedMessagesListState.Loading
|
||||||
|
is AsyncData.Failure -> PinnedMessagesListState.Failed
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
if (timelineItems.data.isEmpty()) {
|
||||||
|
PinnedMessagesListState.Empty
|
||||||
|
} else {
|
||||||
|
PinnedMessagesListState.Filled(
|
||||||
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
|
timelineItems = timelineItems.data,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,36 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.pinned.list
|
package io.element.android.features.messages.impl.pinned.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class PinnedMessagesListState(
|
@Immutable
|
||||||
val timelineRoomInfo: TimelineRoomInfo,
|
sealed interface PinnedMessagesListState {
|
||||||
val timelineItems: ImmutableList<TimelineItem>,
|
data object Failed : PinnedMessagesListState
|
||||||
val eventSink: (PinnedMessagesListEvents) -> Unit,
|
data object Loading : PinnedMessagesListState
|
||||||
|
data object Empty : PinnedMessagesListState
|
||||||
|
data class Filled(
|
||||||
|
val timelineRoomInfo: TimelineRoomInfo,
|
||||||
|
val timelineItems: ImmutableList<TimelineItem>,
|
||||||
|
val eventSink: (PinnedMessagesListEvents) -> Unit,
|
||||||
|
) : PinnedMessagesListState {
|
||||||
|
val loadedPinnedMessagesCount = timelineItems.count { timelineItem -> timelineItem is TimelineItem.Event }
|
||||||
|
}
|
||||||
|
|
||||||
val pinnedMessagesCount: String =
|
@Composable
|
||||||
timelineItems
|
fun title(): String {
|
||||||
.count { timelineItem -> timelineItem is TimelineItem.Event }
|
return when (this) {
|
||||||
.takeIf { it > 0 }
|
is Filled -> {
|
||||||
?.toString()
|
pluralStringResource(id = CommonPlurals.screen_pinned_timeline_screen_title, loadedPinnedMessagesCount, loadedPinnedMessagesCount)
|
||||||
?: ""
|
}
|
||||||
)
|
else -> stringResource(id = CommonStrings.screen_pinned_timeline_screen_title_empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
|
@ -33,9 +34,12 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -63,8 +67,8 @@ fun PinnedMessagesListView(
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -80,7 +84,7 @@ private fun PinnedMessagesListTopBar(
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, state.pinnedMessagesCount),
|
text = state.title(),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium
|
style = ElementTheme.typography.fontBodyLgMedium
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -97,42 +101,87 @@ private fun PinnedMessagesListContent(
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier) {
|
Box(modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
when (state) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
PinnedMessagesListState.Failed -> Unit
|
||||||
state = rememberLazyListState(),
|
PinnedMessagesListState.Empty -> PinnedMessagesListEmpty()
|
||||||
reverseLayout = true,
|
is PinnedMessagesListState.Filled -> PinnedMessagesListLoaded(
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
state = state,
|
||||||
) {
|
onEventClick = onEventClick,
|
||||||
items(
|
onUserDataClick = onUserDataClick,
|
||||||
items = state.timelineItems,
|
onLinkClick = onLinkClick,
|
||||||
contentType = { timelineItem -> timelineItem.contentType() },
|
)
|
||||||
key = { timelineItem -> timelineItem.identifier() },
|
PinnedMessagesListState.Loading -> {
|
||||||
) { timelineItem ->
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
TimelineItemRow(
|
CircularProgressIndicator()
|
||||||
timelineItem = timelineItem,
|
}
|
||||||
timelineRoomInfo = state.timelineRoomInfo,
|
|
||||||
renderReadReceipts = false,
|
|
||||||
isLastOutgoingMessage = false,
|
|
||||||
focusedEventId = null,
|
|
||||||
onClick = onEventClick,
|
|
||||||
onLongClick = {},
|
|
||||||
onUserDataClick = onUserDataClick,
|
|
||||||
onLinkClick = onLinkClick,
|
|
||||||
inReplyToClick = {},
|
|
||||||
onReactionClick = { _, _ -> },
|
|
||||||
onReactionLongClick = { _, _ -> },
|
|
||||||
onMoreReactionsClick = {},
|
|
||||||
onReadReceiptClick = {},
|
|
||||||
eventSink = {},
|
|
||||||
onSwipeToReply = {},
|
|
||||||
onJoinCallClick = {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessagesListEmpty(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 48.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val pinActionText = stringResource(id = CommonStrings.action_pin)
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
title = stringResource(id = CommonStrings.screen_pinned_timeline_empty_state_headline),
|
||||||
|
subTitle = stringResource(id = CommonStrings.screen_pinned_timeline_empty_state_description, pinActionText),
|
||||||
|
iconResourceId = CompoundDrawables.ic_compound_pin,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PinnedMessagesListLoaded(
|
||||||
|
state: PinnedMessagesListState.Filled,
|
||||||
|
onEventClick: (event: TimelineItem.Event) -> Unit,
|
||||||
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onLinkClick: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
state = rememberLazyListState(),
|
||||||
|
reverseLayout = true,
|
||||||
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.timelineItems,
|
||||||
|
contentType = { timelineItem -> timelineItem.contentType() },
|
||||||
|
key = { timelineItem -> timelineItem.identifier() },
|
||||||
|
) { timelineItem ->
|
||||||
|
TimelineItemRow(
|
||||||
|
timelineItem = timelineItem,
|
||||||
|
timelineRoomInfo = state.timelineRoomInfo,
|
||||||
|
renderReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
|
focusedEventId = null,
|
||||||
|
onClick = onEventClick,
|
||||||
|
onLongClick = {},
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
|
onLinkClick = onLinkClick,
|
||||||
|
inReplyToClick = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onReactionLongClick = { _, _ -> },
|
||||||
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
|
eventSink = {},
|
||||||
|
onSwipeToReply = {},
|
||||||
|
onJoinCallClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) =
|
fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) =
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,24 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider<PinnedMessagesListState> {
|
open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider<PinnedMessagesListState> {
|
||||||
override val values: Sequence<PinnedMessagesListState>
|
override val values: Sequence<PinnedMessagesListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
pinnedMessagesListState(),
|
aFailedPinnedMessagesListState(),
|
||||||
|
aLoadingPinnedMessagesListState(),
|
||||||
|
anEmptyPinnedMessagesListState(),
|
||||||
|
aLoadedPinnedMessagesListState()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pinnedMessagesListState(
|
fun aFailedPinnedMessagesListState() = PinnedMessagesListState.Failed
|
||||||
|
|
||||||
|
fun aLoadingPinnedMessagesListState() = PinnedMessagesListState.Loading
|
||||||
|
|
||||||
|
fun anEmptyPinnedMessagesListState() = PinnedMessagesListState.Empty
|
||||||
|
|
||||||
|
fun aLoadedPinnedMessagesListState(
|
||||||
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
||||||
timelineItems: List<TimelineItem> = emptyList(),
|
timelineItems: List<TimelineItem> = emptyList(),
|
||||||
) = PinnedMessagesListState(
|
) = PinnedMessagesListState.Filled(
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
timelineItems = timelineItems.toImmutableList(),
|
timelineItems = timelineItems.toImmutableList(),
|
||||||
eventSink = {}
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemE
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -87,7 +88,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||||
|
|
||||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
val timelineItems by timelineItemsFactory.timelineItems.collectAsState(initial = persistentListOf())
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.factories
|
package io.element.android.features.messages.impl.timeline.factories
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
||||||
|
|
@ -31,9 +28,11 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -46,7 +45,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
private val timelineItemGrouper: TimelineItemGrouper,
|
private val timelineItemGrouper: TimelineItemGrouper,
|
||||||
private val timelineItemIndexer: TimelineItemIndexer,
|
private val timelineItemIndexer: TimelineItemIndexer,
|
||||||
) {
|
) {
|
||||||
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
|
private val _timelineItems = MutableSharedFlow<ImmutableList<TimelineItem>>(replay = 1)
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<TimelineItem>()
|
private val diffCache = MutableListDiffCache<TimelineItem>()
|
||||||
private val diffCacheUpdater = DiffCacheUpdater<MatrixTimelineItem, TimelineItem>(
|
private val diffCacheUpdater = DiffCacheUpdater<MatrixTimelineItem, TimelineItem>(
|
||||||
|
|
@ -61,10 +60,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
val timelineItems: Flow<ImmutableList<TimelineItem>> = _timelineItems.distinctUntilChanged()
|
||||||
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
|
||||||
return timelineItems.collectAsState()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun replaceWith(
|
suspend fun replaceWith(
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
|
@ -102,7 +98,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
||||||
timelineItemIndexer.process(result)
|
timelineItemIndexer.process(result)
|
||||||
this.timelineItems.emit(result)
|
this._timelineItems.emit(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildAndCacheItem(
|
private suspend fun buildAndCacheItem(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue