Pinned events : try sharing pinned events timeline instance
This commit is contained in:
@@ -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.preview.AttachmentsPreviewNode
|
||||
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.report.ReportMessageNode
|
||||
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
||||
@@ -96,6 +97,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
||||
private val mentionSpanTheme: MentionSpanTheme,
|
||||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
@@ -163,6 +165,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
roomMemberProfilesCache.replace(membersState.joinedRoomMembers())
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
pinnedEventsTimelineProvider.launchIn(lifecycleScope)
|
||||
|
||||
}
|
||||
|
||||
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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.messages.impl.pinned.IsPinnedMessagesFeatureEnabled
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -45,46 +46,38 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
class PinnedMessagesBannerPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||
private val isFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val timelineController: PinnedEventsTimelineProvider,
|
||||
) : Presenter<PinnedMessagesBannerState> {
|
||||
private val pinnedItems = mutableStateOf<ImmutableList<PinnedMessagesBannerItem>>(persistentListOf())
|
||||
private val pinnedItems = mutableStateOf<AsyncData<ImmutableList<PinnedMessagesBannerItem>>>(AsyncData.Uninitialized)
|
||||
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesBannerState {
|
||||
val isFeatureEnabled = isFeatureEnabled()
|
||||
val expectedPinnedMessagesCount by remember {
|
||||
room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size }
|
||||
}.collectAsState(initial = 0)
|
||||
|
||||
var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) }
|
||||
var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) }
|
||||
|
||||
PinnedMessagesBannerItemsEffect(
|
||||
isFeatureEnabled = isFeatureEnabled,
|
||||
onItemsChange = { newItems ->
|
||||
val pinnedMessageCount = newItems.size
|
||||
val pinnedMessageCount = newItems.dataOrNull().orEmpty().size
|
||||
if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) {
|
||||
currentPinnedMessageIndex = pinnedMessageCount - 1
|
||||
}
|
||||
pinnedItems.value = newItems
|
||||
},
|
||||
onTimelineFail = { hasTimelineFailed ->
|
||||
hasTimelineFailedToLoad = hasTimelineFailed
|
||||
}
|
||||
)
|
||||
|
||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||
when (event) {
|
||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||
currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(pinnedItems.value.size)
|
||||
val loadedCount = pinnedItems.value.dataOrNull().orEmpty().size
|
||||
currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(loadedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pinnedMessagesBannerState(
|
||||
isFeatureEnabled = isFeatureEnabled,
|
||||
hasTimelineFailed = hasTimelineFailedToLoad,
|
||||
expectedPinnedMessagesCount = expectedPinnedMessagesCount,
|
||||
pinnedItems = pinnedItems.value,
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
@@ -94,63 +87,65 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
||||
|
||||
@Composable
|
||||
private fun pinnedMessagesBannerState(
|
||||
isFeatureEnabled: Boolean,
|
||||
hasTimelineFailed: Boolean,
|
||||
expectedPinnedMessagesCount: Int,
|
||||
pinnedItems: ImmutableList<PinnedMessagesBannerItem>,
|
||||
pinnedItems: AsyncData<ImmutableList<PinnedMessagesBannerItem>>,
|
||||
currentPinnedMessageIndex: Int,
|
||||
eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||
): PinnedMessagesBannerState {
|
||||
val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex)
|
||||
return when {
|
||||
!isFeatureEnabled -> PinnedMessagesBannerState.Hidden
|
||||
hasTimelineFailed -> PinnedMessagesBannerState.Hidden
|
||||
currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded(
|
||||
currentPinnedMessage = currentPinnedMessage,
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
loadedPinnedMessagesCount = pinnedItems.size,
|
||||
eventSink = eventSink
|
||||
)
|
||||
expectedPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden
|
||||
else -> PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount)
|
||||
return when (pinnedItems) {
|
||||
is AsyncData.Failure, is AsyncData.Uninitialized -> PinnedMessagesBannerState.Hidden
|
||||
is AsyncData.Loading -> {
|
||||
if (expectedPinnedMessagesCount == 0) {
|
||||
PinnedMessagesBannerState.Hidden
|
||||
} else {
|
||||
PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount)
|
||||
}
|
||||
}
|
||||
is AsyncData.Success -> {
|
||||
val currentPinnedMessage = pinnedItems.data.getOrNull(currentPinnedMessageIndex)
|
||||
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
|
||||
private fun PinnedMessagesBannerItemsEffect(
|
||||
isFeatureEnabled: Boolean,
|
||||
onItemsChange: (ImmutableList<PinnedMessagesBannerItem>) -> Unit,
|
||||
onTimelineFail: (Boolean) -> Unit,
|
||||
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit,
|
||||
) {
|
||||
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||
val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail)
|
||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
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) {
|
||||
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()
|
||||
AsyncData.Success(pinnedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEach { newItems ->
|
||||
updatedOnItemsChange(newItems)
|
||||
}
|
||||
.onCompletion {
|
||||
pinnedEventsTimeline.close()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,53 +18,118 @@ package io.element.android.features.messages.impl.pinned.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.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.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class PinnedMessagesListPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val timelineItemsFactory: TimelineItemsFactory,
|
||||
private val timelineProvider: PinnedEventsTimelineProvider,
|
||||
) : Presenter<PinnedMessagesListState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesListState {
|
||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
||||
val timelineRoomInfo = remember {
|
||||
TimelineRoomInfo(
|
||||
isDm = room.isDm,
|
||||
name = room.displayName,
|
||||
// We don't need to compute those values
|
||||
userHasPermissionToSendMessage = false,
|
||||
userHasPermissionToSendReaction = false,
|
||||
isCallOngoing = false,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
|
||||
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
|
||||
timelineItemsFactory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = membersState.roomMembers().orEmpty()
|
||||
)
|
||||
items
|
||||
}.launchIn(this)
|
||||
|
||||
var pinnedMessageItems by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<TimelineItem>>>(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
PinnedMessagesListEffect(
|
||||
onItemsChange = { newItems ->
|
||||
pinnedMessageItems = newItems
|
||||
}
|
||||
)
|
||||
|
||||
fun handleEvents(event: PinnedMessagesListEvents) {
|
||||
}
|
||||
|
||||
return PinnedMessagesListState(
|
||||
return pinnedMessagesListState(
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
timelineItems = timelineItems,
|
||||
timelineItems = pinnedMessageItems,
|
||||
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
|
||||
|
||||
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.model.TimelineItem
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class PinnedMessagesListState(
|
||||
val timelineRoomInfo: TimelineRoomInfo,
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val eventSink: (PinnedMessagesListEvents) -> Unit,
|
||||
@Immutable
|
||||
sealed interface PinnedMessagesListState {
|
||||
data object Failed : PinnedMessagesListState
|
||||
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 =
|
||||
timelineItems
|
||||
.count { timelineItem -> timelineItem is TimelineItem.Event }
|
||||
.takeIf { it > 0 }
|
||||
?.toString()
|
||||
?: ""
|
||||
)
|
||||
@Composable
|
||||
fun title(): String {
|
||||
return when (this) {
|
||||
is Filled -> {
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.features.messages.impl.timeline.components.TimelineItemRow
|
||||
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.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
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.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
@@ -63,8 +67,8 @@ fun PinnedMessagesListView(
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -80,7 +84,7 @@ private fun PinnedMessagesListTopBar(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, state.pinnedMessagesCount),
|
||||
text = state.title(),
|
||||
style = ElementTheme.typography.fontBodyLgMedium
|
||||
)
|
||||
},
|
||||
@@ -97,42 +101,87 @@ private fun PinnedMessagesListContent(
|
||||
onLinkClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(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 = {},
|
||||
)
|
||||
Box(modifier.fillMaxSize()) {
|
||||
when (state) {
|
||||
PinnedMessagesListState.Failed -> Unit
|
||||
PinnedMessagesListState.Empty -> PinnedMessagesListEmpty()
|
||||
is PinnedMessagesListState.Filled -> PinnedMessagesListLoaded(
|
||||
state = state,
|
||||
onEventClick = onEventClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
PinnedMessagesListState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@Composable
|
||||
fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) =
|
||||
|
||||
@@ -25,15 +25,24 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider<PinnedMessagesListState> {
|
||||
override val values: Sequence<PinnedMessagesListState>
|
||||
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(),
|
||||
timelineItems: List<TimelineItem> = emptyList(),
|
||||
) = PinnedMessagesListState(
|
||||
) = PinnedMessagesListState.Filled(
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
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.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -87,7 +88,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||
|
||||
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 syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
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.diff.TimelineItemsCacheInvalidator
|
||||
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.timeline.MatrixTimelineItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
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.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -46,7 +45,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||
private val timelineItemGrouper: TimelineItemGrouper,
|
||||
private val timelineItemIndexer: TimelineItemIndexer,
|
||||
) {
|
||||
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
|
||||
private val _timelineItems = MutableSharedFlow<ImmutableList<TimelineItem>>(replay = 1)
|
||||
private val lock = Mutex()
|
||||
private val diffCache = MutableListDiffCache<TimelineItem>()
|
||||
private val diffCacheUpdater = DiffCacheUpdater<MatrixTimelineItem, TimelineItem>(
|
||||
@@ -61,10 +60,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
||||
return timelineItems.collectAsState()
|
||||
}
|
||||
val timelineItems: Flow<ImmutableList<TimelineItem>> = _timelineItems.distinctUntilChanged()
|
||||
|
||||
suspend fun replaceWith(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
@@ -102,7 +98,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||
}
|
||||
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
||||
timelineItemIndexer.process(result)
|
||||
this.timelineItems.emit(result)
|
||||
this._timelineItems.emit(result)
|
||||
}
|
||||
|
||||
private suspend fun buildAndCacheItem(
|
||||
|
||||
Reference in New Issue
Block a user