From 0c5d10031d56d289662d855c22ba45085139f392 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Aug 2024 18:13:51 +0200 Subject: [PATCH 01/22] Pinned events : add the PinnedMessagesTimeline feature classes and branch in the navigation --- .../messages/impl/MessagesFlowNode.kt | 10 +- .../pinned/list/PinnedMessagesListEvents.kt | 19 +++ .../pinned/list/PinnedMessagesListNode.kt | 52 +++++++++ .../list/PinnedMessagesListPresenter.kt | 58 +++++++++ .../pinned/list/PinnedMessagesListState.kt | 25 ++++ .../pinned/list/PinnedMessagesListView.kt | 110 ++++++++++++++++++ .../PinnedMessagesTimelineListProvider.kt | 35 ++++++ 7 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index f022fd0caf..ca4bf907ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -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.list.PinnedMessagesListNode 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.model.TimelineItem @@ -81,7 +82,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize -import timber.log.Timber @ContributesNode(RoomScope::class) class MessagesFlowNode @AssistedInject constructor( @@ -148,6 +148,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class EditPoll(val eventId: EventId) : NavTarget + + @Parcelize + data object PinnedEvents : NavTarget } private val callbacks = plugins() @@ -220,7 +223,7 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onViewAllPinnedEvents() { - Timber.d("On View All Pinned Events not implemented yet.") + backstack.push(NavTarget.PinnedEvents) } } val inputs = MessagesNode.Inputs( @@ -276,6 +279,9 @@ class MessagesFlowNode @AssistedInject constructor( .params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.eventId))) .build() } + NavTarget.PinnedEvents -> { + createNode(buildContext) + } NavTarget.Empty -> { node(buildContext) {} } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt new file mode 100644 index 0000000000..e259df4d62 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt @@ -0,0 +1,19 @@ +/* + * 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.list + +sealed interface PinnedMessagesListEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt new file mode 100644 index 0000000000..5aee2bc3bb --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -0,0 +1,52 @@ +/* + * 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.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class PinnedMessagesListNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: PinnedMessagesListPresenter, + private val timelineItemPresenterFactories: TimelineItemPresenterFactories, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, + ) { + val state = presenter.present() + PinnedMessagesListView( + state = state, + modifier = modifier + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt new file mode 100644 index 0000000000..cde419f463 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -0,0 +1,58 @@ +/* + * 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.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.roomMembers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import javax.inject.Inject + +class PinnedMessagesListPresenter @Inject constructor( + private val room: MatrixRoom, + private val timelineItemsFactory: TimelineItemsFactory, +) : Presenter { + + @Composable + override fun present(): PinnedMessagesListState { + val timelineItems by timelineItemsFactory.collectItemsAsState() + + 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) + } + + fun handleEvents(event: PinnedMessagesListEvents) { + } + + return PinnedMessagesListState( + timelineItems = timelineItems, + eventSink = ::handleEvents + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt new file mode 100644 index 0000000000..4c1d779b62 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -0,0 +1,25 @@ +/* + * 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.list + +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import kotlinx.collections.immutable.ImmutableList + +data class PinnedMessagesListState( + val timelineItems: ImmutableList, + val eventSink: (PinnedMessagesListEvents) -> Unit +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt new file mode 100644 index 0000000000..f5e1d6a173 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -0,0 +1,110 @@ +/* + * 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.list + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +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.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Scaffold + +@Composable +fun PinnedMessagesListView( + state: PinnedMessagesListState, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + ) { padding -> + PinnedMessagesListContent( + state = state, + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + ) + } +} + +@Composable +fun PinnedMessagesListContent( + state: PinnedMessagesListState, + 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 = TimelineRoomInfo( + isDm = false, + name = null, + userHasPermissionToSendMessage = false, + userHasPermissionToSendReaction = false, + isCallOngoing = false, + ), + renderReadReceipts = false, + isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && + state.timelineItems.first().identifier() == timelineItem.identifier(), + focusedEventId = null, + onClick = {}, + onLongClick = {}, + onUserDataClick = { }, + onLinkClick = {}, + inReplyToClick = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + eventSink = {}, + onSwipeToReply = {}, + onJoinCallClick = {}, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) = + ElementPreview { + PinnedMessagesListView( + state = state, + ) + } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt new file mode 100644 index 0000000000..e70e893909 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -0,0 +1,35 @@ +/* + * 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.list + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import kotlinx.collections.immutable.toImmutableList + +open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + pinnedMessagesListState(), + ) +} + +fun pinnedMessagesListState( + timelineItems: List = emptyList(), +) = PinnedMessagesListState( + timelineItems = timelineItems.toImmutableList(), + eventSink = {} +) From 0832fb9f2fc03a529c784237c1574bc6c233559b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Aug 2024 20:40:42 +0200 Subject: [PATCH 02/22] Timeline : add a mode to differentiate between live/focused/pinned --- .../libraries/matrix/api/timeline/Timeline.kt | 6 ++++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 10 +++++----- .../libraries/matrix/impl/timeline/RustTimeline.kt | 12 ++++++------ .../LastForwardIndicatorsPostProcessor.kt | 7 ++++--- .../postprocessor/RoomBeginningPostProcessor.kt | 4 +++- .../RoomBeginningPostProcessorTest.kt | 14 +++++++------- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 9585b2521b..8fc1478bf7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -46,6 +46,12 @@ interface Timeline : AutoCloseable { FORWARDS } + enum class Mode { + LIVE, + FOCUSED_ON_EVENT, + FOCUSED_ON_PINNED_EVENTS + } + val membershipChangeEventReceived: Flow suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result suspend fun paginate(direction: PaginationDirection): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 20dceeeb87..6d0dea6d03 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -153,7 +153,7 @@ class RustMatrixRoom( private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - override val liveTimeline = createTimeline(innerTimeline, isLive = true) { + override val liveTimeline = createTimeline(innerTimeline, mode = Timeline.Mode.LIVE) { _syncUpdateFlow.value = systemClock.epochMillis() } @@ -181,7 +181,7 @@ class RustMatrixRoom( numContextEvents = 50u, internalIdPrefix = "focus_$eventId", ).let { inner -> - createTimeline(inner, isLive = false) + createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_EVENT) } }.mapFailure { it.toFocusEventException() @@ -198,7 +198,7 @@ class RustMatrixRoom( internalIdPrefix = "pinned_events", maxEventsToLoad = 100u, ).let { inner -> - createTimeline(inner, isLive = false) + createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_PINNED_EVENTS) } }.onFailure { if (it is CancellationException) { @@ -655,13 +655,13 @@ class RustMatrixRoom( private fun createTimeline( timeline: InnerTimeline, - isLive: Boolean, + mode: Timeline.Mode, onNewSyncedEvent: () -> Unit = {}, ): Timeline { val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline") return RustTimeline( isKeyBackupEnabled = isKeyBackupEnabled, - isLive = isLive, + mode = mode, matrixRoom = this, systemClock = systemClock, coroutineScope = timelineCoroutineScope, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 8c774d2d29..3f9008ef28 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -85,7 +85,7 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, - private val isLive: Boolean, + mode: Timeline.Mode, systemClock: SystemClock, isKeyBackupEnabled: Boolean, private val matrixRoom: MatrixRoom, @@ -131,21 +131,21 @@ class RustTimeline( onNewSyncedEvent = onNewSyncedEvent, ) - private val roomBeginningPostProcessor = RoomBeginningPostProcessor() + private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode) private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) - private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(isLive) + private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val backPaginationStatus = MutableStateFlow( - Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true) + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.FOCUSED_ON_PINNED_EVENTS) ) private val forwardPaginationStatus = MutableStateFlow( - Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = !isLive) + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT) ) init { coroutineScope.fetchMembers() - if (isLive) { + if (mode == Timeline.Mode.LIVE) { // When timeline is live, we need to listen to the back pagination status as // sdk can automatically paginate backwards. coroutineScope.registerBackPaginationStatusListener() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt index d717fac00d..2dde2c7591 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt @@ -17,21 +17,22 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem /** * This post processor is responsible for adding virtual items to indicate all the previous last forward item. */ class LastForwardIndicatorsPostProcessor( - private val isTimelineLive: Boolean, + private val mode: Timeline.Mode, ) { private val lastForwardIdentifiers = LinkedHashSet() fun process( items: List, ): List { - // If the timeline is live, we don't have any last forward indicator to display - if (isTimelineLive) { + // We don't need to add the last forward indicator if we are not in the FOCUSED_ON_EVENT mode + if (mode != Timeline.Mode.FOCUSED_ON_EVENT) { return items } else { return buildList { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index e3fa9c8072..cec7a79219 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import androidx.annotation.VisibleForTesting import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent @@ -28,13 +29,14 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime * This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs * or add the RoomBeginning item for non DM room. */ -class RoomBeginningPostProcessor { +class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { fun process( items: List, isDm: Boolean, hasMoreToLoadBackwards: Boolean ): List { return when { + mode == Timeline.Mode.FOCUSED_ON_PINNED_EVENTS -> items hasMoreToLoadBackwards -> items isDm -> processForDM(items) else -> processForRoom(items) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index 63b0664bc7..9700b70a77 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -36,7 +36,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEmpty() } @@ -53,7 +53,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(expected) } @@ -64,7 +64,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo( listOf(processor.createRoomBeginningItem()) + timelineItems @@ -76,7 +76,7 @@ class RoomBeginningPostProcessorTest { val timelineItems = listOf( MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(timelineItems) } @@ -87,7 +87,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -97,7 +97,7 @@ class RoomBeginningPostProcessorTest { val timelineItems = listOf( MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -108,7 +108,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor() + val processor = RoomBeginningPostProcessor(mode) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } From 060b0350e02ee987fe834bab853f0af37eb9a49d Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Aug 2024 20:40:59 +0200 Subject: [PATCH 03/22] Pinned messages : start branching ui to the timeline --- .../messages/impl/MessagesFlowNode.kt | 15 +++- .../pinned/list/PinnedMessagesListNode.kt | 48 +++++++++++ .../list/PinnedMessagesListPresenter.kt | 14 +++- .../pinned/list/PinnedMessagesListState.kt | 11 ++- .../pinned/list/PinnedMessagesListView.kt | 81 ++++++++++++++----- .../PinnedMessagesTimelineListProvider.kt | 4 + 6 files changed, 148 insertions(+), 25 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index ca4bf907ac..3b30d11a7f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -280,7 +280,20 @@ class MessagesFlowNode @AssistedInject constructor( .build() } NavTarget.PinnedEvents -> { - createNode(buildContext) + val callback = object : PinnedMessagesListNode.Callback { + override fun onEventClick(event: TimelineItem.Event) { + processEventClick(event) + } + + override fun onUserDataClick(userId: UserId) { + callbacks.forEach { it.onUserDataClick(userId) } + } + + override fun onPermalinkClick(data: PermalinkData) { + callbacks.forEach { it.onPermalinkClick(data) } + } + } + createNode(buildContext, plugins = listOf(callback)) } NavTarget.Empty -> { node(buildContext) {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 5aee2bc3bb..ff53cf0781 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -16,18 +16,27 @@ package io.element.android.features.messages.impl.pinned.list +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.impl.MessagesNode.Callback import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser @ContributesNode(RoomScope::class) class PinnedMessagesListNode @AssistedInject constructor( @@ -35,16 +44,55 @@ class PinnedMessagesListNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: PinnedMessagesListPresenter, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, + private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onEventClick(event: TimelineItem.Event) + fun onUserDataClick(userId: UserId) + fun onPermalinkClick(data: PermalinkData) + } + + private val callbacks = plugins() + + private fun onEventClick(event: TimelineItem.Event) { + return callbacks.forEach { it.onEventClick(event) } + } + + private fun onUserDataClick(userId: UserId) { + callbacks.forEach { it.onUserDataClick(userId) } + } + + private fun onLinkClick(context: Context, url: String) { + when (val permalink = permalinkParser.parse(url)) { + is PermalinkData.UserLink -> { + // Open the room member profile, it will fallback to + // the user profile if the user is not in the room + callbacks.forEach { it.onUserDataClick(permalink.userId) } + } + is PermalinkData.RoomLink -> { + callbacks.forEach { it.onPermalinkClick(permalink) } + } + is PermalinkData.FallbackLink, + is PermalinkData.RoomEmailInviteLink -> { + context.openUrlInExternalApp(url) + } + } + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { + val context = LocalContext.current val state = presenter.present() PinnedMessagesListView( state = state, + onBackClick = ::navigateUp, + onEventClick = ::onEventClick, + onUserDataClick = ::onUserDataClick, + onLinkClick = { url -> onLinkClick(context, url) }, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index cde419f463..35d6c984f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -19,9 +19,12 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory 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.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -35,7 +38,15 @@ class PinnedMessagesListPresenter @Inject constructor( @Composable override fun present(): PinnedMessagesListState { val timelineItems by timelineItemsFactory.collectItemsAsState() - + val timelineRoomInfo = remember { + TimelineRoomInfo( + isDm = room.isDm, + name = room.displayName, + userHasPermissionToSendMessage = false, + userHasPermissionToSendReaction = false, + isCallOngoing = false, + ) + } LaunchedEffect(Unit) { val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect combine(timeline.timelineItems, room.membersStateFlow) { items, membersState -> @@ -51,6 +62,7 @@ class PinnedMessagesListPresenter @Inject constructor( } return PinnedMessagesListState( + timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index 4c1d779b62..80a3578e74 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -16,10 +16,19 @@ package io.element.android.features.messages.impl.pinned.list +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem import kotlinx.collections.immutable.ImmutableList data class PinnedMessagesListState( + val timelineRoomInfo: TimelineRoomInfo, val timelineItems: ImmutableList, - val eventSink: (PinnedMessagesListEvents) -> Unit + val eventSink: (PinnedMessagesListEvents) -> Unit, + + val pinnedMessagesCount: String = + timelineItems + .count { timelineItem -> timelineItem is TimelineItem.Event } + .takeIf { it > 0 } + ?.toString() + ?: "" ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index f5e1d6a173..c86b14a042 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -24,37 +24,77 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn 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.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +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.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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 +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PinnedMessagesListView( state: PinnedMessagesListState, + onBackClick: () -> Unit, + onEventClick: (event: TimelineItem.Event) -> Unit, + onUserDataClick: (UserId) -> Unit, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier, - ) { padding -> - PinnedMessagesListContent( - state = state, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), - ) - } + topBar = { + PinnedMessagesListTopBar(state, onBackClick) + }, + content = { padding -> + PinnedMessagesListContent( + state = state, + onEventClick = onEventClick, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PinnedMessagesListTopBar( + state: PinnedMessagesListState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + title = { + Text( + text = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, state.pinnedMessagesCount), + style = ElementTheme.typography.fontBodyLgMedium + ) + }, + navigationIcon = { BackButton(onClick = onBackClick) }, + modifier = modifier, + ) } @Composable -fun PinnedMessagesListContent( +private fun PinnedMessagesListContent( state: PinnedMessagesListState, + onEventClick: (event: TimelineItem.Event) -> Unit, + onUserDataClick: (UserId) -> Unit, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier) { @@ -71,21 +111,14 @@ fun PinnedMessagesListContent( ) { timelineItem -> TimelineItemRow( timelineItem = timelineItem, - timelineRoomInfo = TimelineRoomInfo( - isDm = false, - name = null, - userHasPermissionToSendMessage = false, - userHasPermissionToSendReaction = false, - isCallOngoing = false, - ), + timelineRoomInfo = state.timelineRoomInfo, renderReadReceipts = false, - isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && - state.timelineItems.first().identifier() == timelineItem.identifier(), + isLastOutgoingMessage = false, focusedEventId = null, - onClick = {}, + onClick = onEventClick, onLongClick = {}, - onUserDataClick = { }, - onLinkClick = {}, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, @@ -106,5 +139,9 @@ fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineSt ElementPreview { PinnedMessagesListView( state = state, + onBackClick = {}, + onEventClick = { }, + onUserDataClick = {}, + onLinkClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt index e70e893909..b7f3124406 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem import kotlinx.collections.immutable.toImmutableList @@ -28,8 +30,10 @@ open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider = emptyList(), ) = PinnedMessagesListState( + timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems.toImmutableList(), eventSink = {} ) From 33ed44c789870dc2e18d0961aa992d575b779577 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2024 21:36:39 +0200 Subject: [PATCH 04/22] Pinned events : try sharing pinned events timeline instance --- .../messages/impl/MessagesFlowNode.kt | 5 + .../pinned/PinnedEventsTimelineProvider.kt | 84 +++++++++++++ .../banner/PinnedMessagesBannerPresenter.kt | 111 ++++++++--------- .../list/PinnedMessagesListPresenter.kt | 89 +++++++++++-- .../pinned/list/PinnedMessagesListState.kt | 39 ++++-- .../pinned/list/PinnedMessagesListView.kt | 117 +++++++++++++----- .../PinnedMessagesTimelineListProvider.kt | 17 ++- .../impl/timeline/TimelinePresenter.kt | 3 +- .../factories/TimelineItemsFactory.kt | 18 ++- 9 files changed, 352 insertions(+), 131 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 3b30d11a7f..c244f73d35 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -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( 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 { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt new file mode 100644 index 0000000000..24060a6a80 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -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> = 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 + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index 13c45e0092..b577995acb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -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 { - private val pinnedItems = mutableStateOf>(persistentListOf()) + private val pinnedItems = mutableStateOf>>(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, + pinnedItems: AsyncData>, 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) -> Unit, - onTimelineFail: (Boolean) -> Unit, + onItemsChange: (AsyncData>) -> 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) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 35d6c984f4..94a7a32cc1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -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 { @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.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>) -> 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>, + 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 + ) + } + } + } + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index 80a3578e74..2e0083842f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -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, - 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, + 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) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index c86b14a042..fbb3586f49 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -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) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt index b7f3124406..bd35407a93 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -25,15 +25,24 @@ import kotlinx.collections.immutable.toImmutableList open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider { override val values: Sequence 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 = emptyList(), -) = PinnedMessagesListState( +) = PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems.toImmutableList(), - eventSink = {} + eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index c7e3dc6e98..5fcf24cdbc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -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(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() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index d3e21d0042..1a3d1bf566 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -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()) + private val _timelineItems = MutableSharedFlow>(replay = 1) private val lock = Mutex() private val diffCache = MutableListDiffCache() private val diffCacheUpdater = DiffCacheUpdater( @@ -61,10 +60,7 @@ class TimelineItemsFactory @Inject constructor( } } - @Composable - fun collectItemsAsState(): State> { - return timelineItems.collectAsState() - } + val timelineItems: Flow> = _timelineItems.distinctUntilChanged() suspend fun replaceWith( timelineItems: List, @@ -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( From 3bcd76d2716679bd918af8e441c26fcd10b8b6cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 27 Aug 2024 10:50:30 +0200 Subject: [PATCH 05/22] Pinned events : fix compilation --- .../features/messages/impl/pinned/list/PinnedMessagesListView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index fbb3586f49..e9c5bd40a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -177,6 +177,7 @@ private fun PinnedMessagesListLoaded( eventSink = {}, onSwipeToReply = {}, onJoinCallClick = {}, + onShieldClick = {}, ) } } From 3cc749dd0ad1600d937bc56d631d8fce7df1ab61 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 28 Aug 2024 21:59:10 +0200 Subject: [PATCH 06/22] Pinned events : start displaying actions in timeline --- .../messages/impl/MessagesFlowNode.kt | 50 ++++++-- .../features/messages/impl/MessagesNode.kt | 5 +- .../messages/impl/MessagesPresenter.kt | 5 +- .../impl/actionlist/ActionListPresenter.kt | 113 ++++++++++-------- .../actionlist/model/TimelineItemAction.kt | 1 + .../model/TimelineItemActionPostProcessor.kt | 28 +++++ .../impl/forward/ForwardMessagesNode.kt | 10 +- .../impl/forward/ForwardMessagesPresenter.kt | 9 +- .../pinned/PinnedEventsTimelineProvider.kt | 17 ++- .../pinned/list/PinnedMessagesListEvents.kt | 8 +- .../list/PinnedMessagesListNavigator.kt | 26 ++++ .../pinned/list/PinnedMessagesListNode.kt | 28 ++++- .../list/PinnedMessagesListPresenter.kt | 100 +++++++++++++++- .../pinned/list/PinnedMessagesListState.kt | 4 + ...MessagesListTimelineActionPostProcessor.kt | 41 +++++++ .../pinned/list/PinnedMessagesListView.kt | 32 ++++- .../PinnedMessagesTimelineListProvider.kt | 7 ++ .../impl/timeline/TimelinePresenter.kt | 2 + .../messages/impl/timeline/TimelineState.kt | 2 + .../impl/timeline/TimelineStateProvider.kt | 3 + .../poll/impl/history/PollHistoryPresenter.kt | 5 +- .../core/coroutine/DerivedStateFlow.kt | 55 +++++++++ libraries/matrix/api/build.gradle.kts | 1 + .../matrix/api/timeline/TimelineProvider.kt | 8 +- 24 files changed, 474 insertions(+), 86 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index c244f73d35..5472741e02 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -21,12 +21,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope +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.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -44,6 +46,7 @@ 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.TimelineController import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -69,6 +72,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache @@ -98,9 +102,10 @@ class MessagesFlowNode @AssistedInject constructor( private val roomMemberProfilesCache: RoomMemberProfilesCache, private val mentionSpanTheme: MentionSpanTheme, private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, + private val timelineController: TimelineController, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Messages, + initialElement = NavTarget.Messages(overriddenFocusedEventId = null), savedStateMap = buildContext.savedStateMap, ), overlay = Overlay( @@ -118,7 +123,7 @@ class MessagesFlowNode @AssistedInject constructor( data object Empty : NavTarget @Parcelize - data object Messages : NavTarget + data class Messages(val overriddenFocusedEventId: EventId?) : NavTarget @Parcelize data class MediaViewer( @@ -137,7 +142,7 @@ class MessagesFlowNode @AssistedInject constructor( data class EventDebugInfo(val eventId: EventId?, val debugInfo: TimelineItemDebugInfo) : NavTarget @Parcelize - data class ForwardEvent(val eventId: EventId) : NavTarget + data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget @Parcelize data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget @@ -159,7 +164,11 @@ class MessagesFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - + lifecycle.subscribe( + onDestroy = { + timelineController.close() + } + ) room.membersStateFlow .onEach { membersState -> roomMemberProfilesCache.replace(membersState.joinedRoomMembers()) @@ -167,7 +176,6 @@ class MessagesFlowNode @AssistedInject constructor( .launchIn(lifecycleScope) pinnedEventsTimelineProvider.launchIn(lifecycleScope) - } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -199,7 +207,7 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onForwardEventClick(eventId: EventId) { - backstack.push(NavTarget.ForwardEvent(eventId)) + backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false)) } override fun onReportMessage(eventId: EventId, senderId: UserId) { @@ -232,7 +240,7 @@ class MessagesFlowNode @AssistedInject constructor( } } val inputs = MessagesNode.Inputs( - focusedEventId = inputs.focusedEventId, + focusedEventId = navTarget.overriddenFocusedEventId ?: inputs.focusedEventId, ) createNode(buildContext, listOf(callback, inputs)) } @@ -259,7 +267,12 @@ class MessagesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(inputs)) } is NavTarget.ForwardEvent -> { - val inputs = ForwardMessagesNode.Inputs(navTarget.eventId) + val timelineProvider = if (navTarget.fromPinnedEvents) { + pinnedEventsTimelineProvider + } else { + timelineController + } + val inputs = ForwardMessagesNode.Inputs(navTarget.eventId, timelineProvider) val callback = object : ForwardMessagesNode.Callback { override fun onForwardedToSingleRoom(roomId: RoomId) { callbacks.forEach { it.onForwardedToSingleRoom(roomId) } @@ -294,8 +307,25 @@ class MessagesFlowNode @AssistedInject constructor( callbacks.forEach { it.onUserDataClick(userId) } } - override fun onPermalinkClick(data: PermalinkData) { - callbacks.forEach { it.onPermalinkClick(data) } + override fun onViewInTimelineClick(eventId: EventId) { + backstack.newRoot(NavTarget.Messages(overriddenFocusedEventId = eventId)) + } + + override fun onRoomPermalinkClick(data: PermalinkData.RoomLink) { + if (room.matches(data.roomIdOrAlias)) { + val eventId = data.eventId + backstack.newRoot(NavTarget.Messages(overriddenFocusedEventId = eventId)) + } else { + callbacks.forEach { it.onPermalinkClick(data) } + } + } + + override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { + backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) + } + + override fun onForwardEventClick(eventId: EventId) { + backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true)) } } createNode(buildContext, plugins = listOf(callback)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index d722a5b7a0..96cd9edfa9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -37,7 +37,6 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents -import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories @@ -75,7 +74,6 @@ class MessagesNode @AssistedInject constructor( private val permalinkParser: PermalinkParser, @ApplicationContext private val context: Context, - private val timelineController: TimelineController, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) private val callbacks = plugins() @@ -107,7 +105,6 @@ class MessagesNode @AssistedInject constructor( analyticsService.capture(room.toAnalyticsViewRoom()) }, onDestroy = { - timelineController.close() mediaPlayer.close() } ) @@ -202,6 +199,8 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onJoinCallClick(room.roomId) } } + + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index c17ac1035b..946617c1ca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -38,6 +38,7 @@ import io.element.android.features.messages.api.timeline.HtmlConverterProvider import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -98,7 +99,7 @@ class MessagesPresenter @AssistedInject constructor( private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, private val typingNotificationPresenter: TypingNotificationPresenter, - private val actionListPresenter: ActionListPresenter, + private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, @@ -114,6 +115,7 @@ class MessagesPresenter @AssistedInject constructor( private val permalinkParser: PermalinkParser, ) : Presenter { private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) + private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default) @AssistedFactory interface Factory { @@ -286,6 +288,7 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState) TimelineItemAction.Pin -> handlePinAction(targetEvent) TimelineItemAction.Unpin -> handleUnpinAction(targetEvent) + TimelineItemAction.ViewInTimeline -> Unit } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 124f4e911d..1f94d2a847 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -23,8 +23,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent @@ -47,13 +51,19 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -class ActionListPresenter @Inject constructor( +class ActionListPresenter @AssistedInject constructor( + @Assisted + private val postProcessor: TimelineItemActionPostProcessor, private val appPreferencesStore: AppPreferencesStore, private val featureFlagsService: FeatureFlagService, private val room: MatrixRoom, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter + } + @Composable override fun present(): ActionListState { val localCoroutineScope = rememberCoroutineScope() @@ -105,6 +115,7 @@ class ActionListPresenter @Inject constructor( isPinnedEventsEnabled = isPinnedEventsEnabled, isEventPinned = pinnedEventIds.contains(timelineItem.eventId), ) + val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.isRemote && timelineItem.content.canReact() @@ -118,57 +129,59 @@ class ActionListPresenter @Inject constructor( target.value = ActionListState.Target.None } } -} -private fun buildActions( - timelineItem: TimelineItem.Event, - usersEventPermissions: UserEventPermissions, - isDeveloperModeEnabled: Boolean, - isPinnedEventsEnabled: Boolean, - isEventPinned: Boolean, -): List { - val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther - return buildList { - if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) { - if (timelineItem.isThreaded) { - add(TimelineItemAction.ReplyInThread) - } else { - add(TimelineItemAction.Reply) + private fun buildActions( + timelineItem: TimelineItem.Event, + usersEventPermissions: UserEventPermissions, + isDeveloperModeEnabled: Boolean, + isPinnedEventsEnabled: Boolean, + isEventPinned: Boolean, + ): List { + val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther + return buildList { + if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) { + if (timelineItem.isThreaded) { + add(TimelineItemAction.ReplyInThread) + } else { + add(TimelineItemAction.Reply) + } + } + if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) { + add(TimelineItemAction.Forward) + } + if (timelineItem.isEditable) { + add(TimelineItemAction.Edit) + } + if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) { + add(TimelineItemAction.EndPoll) + } + val canPinUnpin = isPinnedEventsEnabled && usersEventPermissions.canPinUnpin && timelineItem.isRemote + if (canPinUnpin) { + if (isEventPinned) { + add(TimelineItemAction.Unpin) + } else { + add(TimelineItemAction.Pin) + } + } + if (timelineItem.content.canBeCopied()) { + add(TimelineItemAction.Copy) + } + if (timelineItem.isRemote) { + add(TimelineItemAction.CopyLink) + } + if (isDeveloperModeEnabled) { + add(TimelineItemAction.ViewSource) + } + if (!timelineItem.isMine) { + add(TimelineItemAction.ReportContent) + } + if (canRedact) { + add(TimelineItemAction.Redact) } } - if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) { - add(TimelineItemAction.Forward) - } - if (timelineItem.isEditable) { - add(TimelineItemAction.Edit) - } - if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) { - add(TimelineItemAction.EndPoll) - } - val canPinUnpin = isPinnedEventsEnabled && usersEventPermissions.canPinUnpin && timelineItem.isRemote - if (canPinUnpin) { - if (isEventPinned) { - add(TimelineItemAction.Unpin) - } else { - add(TimelineItemAction.Pin) - } - } - if (timelineItem.content.canBeCopied()) { - add(TimelineItemAction.Copy) - } - if (timelineItem.isRemote) { - add(TimelineItemAction.CopyLink) - } - if (isDeveloperModeEnabled) { - add(TimelineItemAction.ViewSource) - } - if (!timelineItem.isMine) { - add(TimelineItemAction.ReportContent) - } - if (canRedact) { - add(TimelineItemAction.Redact) - } - }.postFilter(timelineItem.content) + .postFilter(timelineItem.content) + .let(postProcessor::process) + } } /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index a650dc88eb..0805a74655 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -29,6 +29,7 @@ sealed class TimelineItemAction( @DrawableRes val icon: Int, val destructive: Boolean = false ) { + data object ViewInTimeline: TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on) data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward) data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy) data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt new file mode 100644 index 0000000000..0b05829f55 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt @@ -0,0 +1,28 @@ +/* + * 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.actionlist.model + +fun interface TimelineItemActionPostProcessor { + fun process(actions: List): List + + object Default : TimelineItemActionPostProcessor { + override fun process(actions: List): List { + return actions + } + } + +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt index 37aabfd4b8..b574f1b6de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt @@ -34,8 +34,11 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode +import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -59,10 +62,13 @@ class ForwardMessagesNode @AssistedInject constructor( fun onForwardedToSingleRoom(roomId: RoomId) } - data class Inputs(val eventId: EventId) : NodeInputs + data class Inputs( + val eventId: EventId, + val timelineProvider: TimelineProvider, + ) : NodeInputs private val inputs = inputs() - private val presenter = presenterFactory.create(inputs.eventId.value) + private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider) private val callbacks = plugins.filterIsInstance() override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index 3b311c7ced..c6e02fdc18 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.forward import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -27,23 +28,27 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import java.sql.Time class ForwardMessagesPresenter @AssistedInject constructor( @Assisted eventId: String, + @Assisted private val timelineProvider: TimelineProvider, private val appCoroutineScope: CoroutineScope, - private val timelineProvider: TimelineProvider, ) : Presenter { private val eventId: EventId = EventId(eventId) @AssistedFactory interface Factory { - fun create(eventId: String): ForwardMessagesPresenter + fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter } private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt index 24060a6a80..1e26e03600 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -18,15 +18,17 @@ 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.core.coroutine.mapState 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 io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion @@ -38,9 +40,16 @@ class PinnedEventsTimelineProvider @Inject constructor( private val room: MatrixRoom, private val networkMonitor: NetworkMonitor, private val featureFlagService: FeatureFlagService, -) { +) : TimelineProvider { private val _timelineStateFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) + override fun activeTimelineFlow(): StateFlow { + return _timelineStateFlow + .mapState { value -> + value.dataOrNull() + } + } + val timelineStateFlow = _timelineStateFlow fun launchIn(scope: CoroutineScope) { @@ -56,12 +65,12 @@ class PinnedEventsTimelineProvider @Inject constructor( } } .onCompletion { - invokeOnTimeline { it.close() } + invokeOnTimeline { close() } } .launchIn(scope) } - suspend fun invokeOnTimeline(action: suspend (Timeline) -> Unit) { + suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) { when (val asyncTimeline = timelineStateFlow.value) { is AsyncData.Success -> action(asyncTimeline.data) else -> Unit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt index e259df4d62..54149fd260 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt @@ -16,4 +16,10 @@ package io.element.android.features.messages.impl.pinned.list -sealed interface PinnedMessagesListEvents +import io.element.android.features.messages.impl.MessagesEvents +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +sealed interface PinnedMessagesListEvents { + data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt new file mode 100644 index 0000000000..0320ec8c01 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt @@ -0,0 +1,26 @@ +/* + * 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.list + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo + +interface PinnedMessagesListNavigator { + fun onViewInTimelineClick(eventId: EventId) + fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) + fun onForwardEventClick(eventId: EventId) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index ff53cf0781..6d902ec332 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -28,31 +28,37 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.messages.impl.MessagesNode.Callback import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @ContributesNode(RoomScope::class) class PinnedMessagesListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PinnedMessagesListPresenter, + presenterFactory: PinnedMessagesListPresenter.Factory, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val permalinkParser: PermalinkParser, -) : Node(buildContext, plugins = plugins) { +) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { fun onEventClick(event: TimelineItem.Event) fun onUserDataClick(userId: UserId) - fun onPermalinkClick(data: PermalinkData) + fun onViewInTimelineClick(eventId: EventId) + fun onRoomPermalinkClick(data: PermalinkData.RoomLink) + fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) + fun onForwardEventClick(eventId: EventId) } + private val presenter = presenterFactory.create(this) private val callbacks = plugins() private fun onEventClick(event: TimelineItem.Event) { @@ -71,7 +77,7 @@ class PinnedMessagesListNode @AssistedInject constructor( callbacks.forEach { it.onUserDataClick(permalink.userId) } } is PermalinkData.RoomLink -> { - callbacks.forEach { it.onPermalinkClick(permalink) } + callbacks.forEach { it.onRoomPermalinkClick(permalink) } } is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { @@ -80,6 +86,18 @@ class PinnedMessagesListNode @AssistedInject constructor( } } + override fun onViewInTimelineClick(eventId: EventId) { + callbacks.forEach { it.onViewInTimelineClick(eventId) } + } + + override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { + callbacks.forEach { it.onShowEventDebugInfoClick(eventId, debugInfo) } + } + + override fun onForwardEventClick(eventId: EventId) { + callbacks.forEach { it.onForwardEventClick(eventId) } + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 94a7a32cc1..5e61b4903b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -18,41 +18,69 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.messages.impl.UserEventPermissions +import io.element.android.features.messages.impl.actionlist.ActionListPresenter +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction 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.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage 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.powerlevels.canPinUnpin +import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther +import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope 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 +import kotlinx.coroutines.launch +import timber.log.Timber -class PinnedMessagesListPresenter @Inject constructor( +class PinnedMessagesListPresenter @AssistedInject constructor( + @Assisted private val navigator: PinnedMessagesListNavigator, private val room: MatrixRoom, private val timelineItemsFactory: TimelineItemsFactory, private val timelineProvider: PinnedEventsTimelineProvider, + private val snackbarDispatcher: SnackbarDispatcher, + actionListPresenterFactory: ActionListPresenter.Factory, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter + } + + private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor) + @Composable override fun present(): PinnedMessagesListState { val timelineRoomInfo = remember { TimelineRoomInfo( isDm = room.isDm, name = room.displayName, + isMainTimeline = false, // We don't need to compute those values userHasPermissionToSendMessage = false, userHasPermissionToSendReaction = false, @@ -60,6 +88,9 @@ class PinnedMessagesListPresenter @Inject constructor( ) } + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val userEventPermissions by userEventPermissions(syncUpdateFlow.value) + var pinnedMessageItems by remember { mutableStateOf>>(AsyncData.Uninitialized) } @@ -70,16 +101,76 @@ class PinnedMessagesListPresenter @Inject constructor( } ) + val coroutineScope = rememberCoroutineScope() fun handleEvents(event: PinnedMessagesListEvents) { + when (event) { + is PinnedMessagesListEvents.HandleAction -> coroutineScope.handleTimelineAction(event.action, event.event) + } } return pinnedMessagesListState( timelineRoomInfo = timelineRoomInfo, + userEventPermissions = userEventPermissions, timelineItems = pinnedMessageItems, eventSink = ::handleEvents ) } + private fun CoroutineScope.handleTimelineAction( + action: TimelineItemAction, + targetEvent: TimelineItem.Event, + ) = launch { + when (action) { + TimelineItemAction.Redact -> handleActionRedact(targetEvent) + TimelineItemAction.ViewSource -> { + navigator.onShowEventDebugInfoClick(targetEvent.eventId, targetEvent.debugInfo) + } + TimelineItemAction.Forward -> { + targetEvent.eventId?.let { eventId -> + navigator.onForwardEventClick(eventId) + } + } + TimelineItemAction.Unpin -> handleUnpinAction(targetEvent) + TimelineItemAction.ViewInTimeline -> { + targetEvent.eventId?.let { eventId -> + navigator.onViewInTimelineClick(eventId) + } + } + else -> Unit + } + } + + private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) { + if (targetEvent.eventId == null) return + timelineProvider.invokeOnTimeline { + unpinEvent(targetEvent.eventId) + .onFailure { + Timber.e(it, "Failed to unpin event ${targetEvent.eventId}") + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + } + } + } + + private suspend fun handleActionRedact(event: TimelineItem.Event) { + timelineProvider.invokeOnTimeline { + redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null) + .onFailure { Timber.e(it) } + } + } + + @Composable + private fun userEventPermissions(updateKey: Long): State { + return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) { + value = UserEventPermissions( + canSendMessage = false, + canSendReaction = false, + canRedactOwn = room.canRedactOwn().getOrElse { false }, + canRedactOther = room.canRedactOther().getOrElse { false }, + canPinUnpin = room.canPinUnpin().getOrElse { false }, + ) + } + } + @Composable private fun PinnedMessagesListEffect(onItemsChange: (AsyncData>) -> Unit) { val updatedOnItemsChange by rememberUpdatedState(onItemsChange) @@ -111,8 +202,10 @@ class PinnedMessagesListPresenter @Inject constructor( } } + @Composable private fun pinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo, + userEventPermissions: UserEventPermissions, timelineItems: AsyncData>, eventSink: (PinnedMessagesListEvents) -> Unit ): PinnedMessagesListState { @@ -123,9 +216,12 @@ class PinnedMessagesListPresenter @Inject constructor( if (timelineItems.data.isEmpty()) { PinnedMessagesListState.Empty } else { + val actionListState = actionListPresenter.present() PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, + userEventPermissions = userEventPermissions, timelineItems = timelineItems.data, + actionListState = actionListState, eventSink = eventSink ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index 2e0083842f..78e6304512 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -20,6 +20,8 @@ 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.UserEventPermissions +import io.element.android.features.messages.impl.actionlist.ActionListState 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 @@ -33,7 +35,9 @@ sealed interface PinnedMessagesListState { data object Empty : PinnedMessagesListState data class Filled( val timelineRoomInfo: TimelineRoomInfo, + val userEventPermissions: UserEventPermissions, val timelineItems: ImmutableList, + val actionListState: ActionListState, val eventSink: (PinnedMessagesListEvents) -> Unit, ) : PinnedMessagesListState { val loadedPinnedMessagesCount = timelineItems.count { timelineItem -> timelineItem is TimelineItem.Event } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt new file mode 100644 index 0000000000..da6e13aac2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt @@ -0,0 +1,41 @@ +/* + * 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.list + +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor + +object PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProcessor { + + override fun process(actions: List): List { + return buildList { + add(TimelineItemAction.ViewInTimeline) + addAll(actions.filter(::predicate)) + } + } + + private fun predicate(action: TimelineItemAction): Boolean { + return when (action) { + is TimelineItemAction.Pin, + is TimelineItemAction.Unpin, + is TimelineItemAction.Redact, + is TimelineItemAction.Forward, + is TimelineItemAction.ViewSource -> true + else -> false + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index e9c5bd40a6..5518e2b36a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -32,6 +32,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.actionlist.ActionListEvents +import io.element.android.features.messages.impl.actionlist.ActionListView +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction 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 @@ -148,6 +151,33 @@ private fun PinnedMessagesListLoaded( onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { + fun onActionSelected(timelineItemAction: TimelineItemAction, event: TimelineItem.Event) { + state.actionListState.eventSink( + ActionListEvents.Clear + ) + state.eventSink( + PinnedMessagesListEvents.HandleAction( + action = timelineItemAction, + event = event, + ) + ) + } + + fun onMessageLongClick(event: TimelineItem.Event) { + state.actionListState.eventSink( + ActionListEvents.ComputeForMessage( + event = event, + userEventPermissions = state.userEventPermissions, + ) + ) + } + + ActionListView( + state = state.actionListState, + onSelectAction = ::onActionSelected, + onCustomReactionClick = {}, + onEmojiReactionClick = { _, _ -> }, + ) LazyColumn( modifier = modifier.fillMaxSize(), state = rememberLazyListState(), @@ -166,7 +196,7 @@ private fun PinnedMessagesListLoaded( isLastOutgoingMessage = false, focusedEventId = null, onClick = onEventClick, - onLongClick = {}, + onLongClick = ::onMessageLongClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, inReplyToClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt index bd35407a93..fe336d8e37 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -17,6 +17,9 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.UserEventPermissions +import io.element.android.features.messages.impl.actionlist.ActionListState +import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -41,8 +44,12 @@ fun anEmptyPinnedMessagesListState() = PinnedMessagesListState.Empty fun aLoadedPinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), timelineItems: List = emptyList(), + actionListState: ActionListState = anActionListState(), + aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT, ) = PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems.toImmutableList(), + actionListState = actionListState, + userEventPermissions = aUserEventPermissions, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 5fcf24cdbc..5324ef44fa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType 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.timeline.ReceiptType +import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendMessageAsState @@ -221,6 +222,7 @@ class TimelinePresenter @AssistedInject constructor( userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, isCallOngoing = roomInfo?.hasRoomCall.orFalse(), + isMainTimeline = true ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 74f8fda0b4..114dc08a98 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration @@ -74,4 +75,5 @@ data class TimelineRoomInfo( val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendReaction: Boolean, val isCallOngoing: Boolean, + val isMainTimeline: Boolean, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index f1abc52725..d697eb5554 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @@ -241,10 +242,12 @@ internal fun aTimelineRoomInfo( name: String = "Room name", isDm: Boolean = false, userHasPermissionToSendMessage: Boolean = true, + isMainTimeline: Boolean = true, ) = TimelineRoomInfo( isDm = isDm, name = name, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = true, isCallOngoing = false, + isMainTimeline = isMainTimeline, ) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index b1579d6cdb..2a76196376 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryFilter import io.element.android.features.poll.impl.history.model.PollHistoryItems import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope @@ -44,11 +45,11 @@ class PollHistoryPresenter @Inject constructor( private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, private val pollHistoryItemFactory: PollHistoryItemsFactory, - private val timelineProvider: TimelineProvider, + private val room: MatrixRoom, ) : Presenter { @Composable override fun present(): PollHistoryState { - val timeline by timelineProvider.activeTimelineFlow().collectAsState() + val timeline = room.liveTimeline val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt new file mode 100644 index 0000000000..4eb13be279 --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt @@ -0,0 +1,55 @@ +/* + * 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.libraries.core.coroutine + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * A [StateFlow] that derives its value from a [Flow]. + * Useful when you want to apply transformations to a [Flow] and expose it as a [StateFlow]. + */ +class DerivedStateFlow( + private val getValue: () -> T, + private val flow: Flow +) : StateFlow { + + override val replayCache: List + get() = listOf(value) + + override val value: T + get() = getValue() + + override suspend fun collect(collector: FlowCollector): Nothing { + coroutineScope { flow.distinctUntilChanged().stateIn(this).collect(collector) } + } +} + +/** + * Maps the value of a [StateFlow] to a new value and returns a new [StateFlow] with the mapped value. + */ +fun StateFlow.mapState(transform: (a: T1) -> R): StateFlow { + return DerivedStateFlow( + getValue = { transform(this.value) }, + flow = this.map { a -> transform(a) } + ) +} diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index b551dfa919..11a557dd6f 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(libs.serialization.json) api(projects.libraries.sessionStorage.api) implementation(libs.coroutines.core) + api(projects.libraries.architecture) testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt index ebfaca48be..d5e953a8ee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -17,14 +17,16 @@ package io.element.android.libraries.matrix.api.timeline import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first /** * This interface defines a way to get the active timeline. - * It could be the current room timeline, or a timeline for a specific event. + * It could be the live timeline, a pinned timeline or a detached timeline. + * By default, the active timeline is the live timeline. */ interface TimelineProvider { - fun activeTimelineFlow(): StateFlow + fun activeTimelineFlow(): StateFlow } -suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first() +suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().filterNotNull().first() From 7b64685777a3c284db313e3fbb08b12889933a52 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 30 Aug 2024 18:27:19 +0200 Subject: [PATCH 07/22] Pinned events : simplify poll content view --- .../pinned/list/PinnedMessagesListView.kt | 40 ++++++++++- .../components/TimelineItemEventRow.kt | 39 +++++----- .../TimelineItemGroupedEventsRow.kt | 24 ++++++- .../timeline/components/TimelineItemRow.kt | 16 ++++- .../poll/api/pollcontent/PollContentView.kt | 34 +-------- .../poll/api/pollcontent/PollTitleView.kt | 71 +++++++++++++++++++ 6 files changed, 167 insertions(+), 57 deletions(-) create mode 100644 features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollTitleView.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index 5518e2b36a..9ccc0ab767 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -36,7 +36,11 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.components.TimelineItemRow +import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent +import io.element.android.features.poll.api.pollcontent.PollTitleView 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 @@ -70,8 +74,8 @@ fun PinnedMessagesListView( onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), + .padding(padding) + .consumeWindowInsets(padding), ) } ) @@ -208,11 +212,43 @@ private fun PinnedMessagesListLoaded( onSwipeToReply = {}, onJoinCallClick = {}, onShieldClick = {}, + eventContentView = { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentViewWrapper( + event = event, + onLinkClick = onLinkClick, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) } } } +@Composable +private fun TimelineItemEventContentViewWrapper( + event: TimelineItem.Event, + onLinkClick: (String) -> Unit, + modifier: Modifier = Modifier, + onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, +) { + if (event.content is TimelineItemPollContent) { + PollTitleView( + title = event.content.question, + isPollEnded = event.content.isEnded, + modifier = modifier + ) + } else { + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = { }, + modifier = modifier, + onContentLayoutChange = onContentLayoutChange + ) + } +} + @PreviewsDayNight @Composable fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 8a6fccf2eb..15caee8091 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -129,7 +129,16 @@ fun TimelineItemEventRow( onReadReceiptClick: (event: TimelineItem.Event) -> Unit, onSwipeToReply: () -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { val coroutineScope = rememberCoroutineScope() val interactionSource = remember { MutableInteractionSource() } @@ -188,8 +197,7 @@ fun TimelineItemEventRow( onReactionClick = { emoji -> onReactionClick(emoji, event) }, onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClick = { onMoreReactionsClick(event) }, - onLinkClick = onLinkClick, - eventSink = eventSink, + eventContentView = eventContentView, ) } } @@ -207,8 +215,7 @@ fun TimelineItemEventRow( onReactionClick = { emoji -> onReactionClick(emoji, event) }, onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClick = { onMoreReactionsClick(event) }, - onLinkClick = onLinkClick, - eventSink = eventSink, + eventContentView = eventContentView, ) } // Read receipts / Send state @@ -263,9 +270,8 @@ private fun TimelineItemEventRowContent( onReactionClick: (emoji: String) -> Unit, onReactionLongClick: (emoji: String) -> Unit, onMoreReactionsClick: (event: TimelineItem.Event) -> Unit, - onLinkClick: (String) -> Unit, - eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, + eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit, ) { fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) { end.linkTo(parent.end) @@ -328,8 +334,7 @@ private fun TimelineItemEventRowContent( onShieldClick = onShieldClick, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, - onLinkClick = onLinkClick, - eventSink = eventSink, + eventContentView = eventContentView, ) } @@ -389,12 +394,11 @@ private fun MessageEventBubbleContent( onShieldClick: (MessageShield) -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, - onLinkClick: (String) -> Unit, - eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @SuppressLint("ModifierParameter") // need to rename this modifier to prevent linter false positives @Suppress("ModifierNaming") bubbleModifier: Modifier = Modifier, + eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit, ) { // Long clicks are not not automatically propagated from a `clickable` // to its `combinedClickable` parent so we do it manually @@ -521,15 +525,10 @@ private fun MessageEventBubbleContent( onShieldClick = onShieldClick, canShrinkContent = canShrinkContent, modifier = timestampLayoutModifier, - ) { onContentLayoutChange -> - TimelineItemEventContentView( - content = event.content, - onLinkClick = onLinkClick, - eventSink = eventSink, - onContentLayoutChange = onContentLayoutChange, - modifier = contentModifier - ) - } + content = { onContentLayoutChange -> + eventContentView(contentModifier, onContentLayoutChange) + } + ) } val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index bb1cb2b2df..478f745ed6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -28,7 +28,9 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aGroupedEvents import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -56,7 +58,16 @@ fun TimelineItemGroupedEventsRow( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) } @@ -84,6 +95,7 @@ fun TimelineItemGroupedEventsRow( onReadReceiptClick = onReadReceiptClick, eventSink = eventSink, modifier = modifier, + eventContentView = eventContentView, ) } @@ -108,6 +120,15 @@ private fun TimelineItemGroupedEventsRowContent( onReadReceiptClick: (TimelineItem.Event) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { Column(modifier = modifier.animateContentSize()) { GroupHeaderView( @@ -142,6 +163,7 @@ private fun TimelineItemGroupedEventsRowContent( eventSink = eventSink, onSwipeToReply = {}, onJoinCallClick = {}, + eventContentView = eventContentView, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 9cbcf20bef..8f659c594f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView +import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent @@ -59,7 +61,16 @@ internal fun TimelineItemRow( onSwipeToReply: (TimelineItem.Event) -> Unit, onJoinCallClick: () -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { val focusedEventOffset = if ((timelineItem as? TimelineItem.Event)?.showSenderInformation == true) { @@ -122,6 +133,9 @@ internal fun TimelineItemRow( onReadReceiptClick = onReadReceiptClick, onSwipeToReply = { onSwipeToReply(timelineItem) }, eventSink = eventSink, + eventContentView = { contentModifier, onContentLayoutChange -> + eventContentView(timelineItem, contentModifier, onContentLayoutChange) + }, ) } } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt index e5c445bd4e..7d8394f30d 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt @@ -19,10 +19,8 @@ package io.element.android.features.poll.api.pollcontent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable @@ -36,12 +34,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer @@ -117,7 +113,7 @@ fun PollContentView( modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - PollTitle(title = question, isPollEnded = isPollEnded) + PollTitleView(title = question, isPollEnded = isPollEnded) PollAnswers(answerItems = answerItems, onSelectAnswer = ::onSelectAnswer) @@ -139,34 +135,6 @@ fun PollContentView( } } -@Composable -private fun PollTitle( - title: String, - isPollEnded: Boolean, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - if (isPollEnded) { - Icon( - imageVector = CompoundIcons.PollsEnd(), - contentDescription = stringResource(id = CommonStrings.a11y_poll_end), - modifier = Modifier.size(22.dp) - ) - } else { - Icon( - imageVector = CompoundIcons.Polls(), - contentDescription = stringResource(id = CommonStrings.a11y_poll), - modifier = Modifier.size(22.dp) - ) - } - Text( - text = title, - style = ElementTheme.typography.fontBodyLgMedium - ) - } -} - @Composable private fun PollAnswers( answerItems: ImmutableList, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollTitleView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollTitleView.kt new file mode 100644 index 0000000000..5a772dec26 --- /dev/null +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollTitleView.kt @@ -0,0 +1,71 @@ +/* + * 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.poll.api.pollcontent + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun PollTitleView( + title: String, + isPollEnded: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (isPollEnded) { + Icon( + imageVector = CompoundIcons.PollsEnd(), + contentDescription = stringResource(id = CommonStrings.a11y_poll_end), + modifier = Modifier.size(22.dp) + ) + } else { + Icon( + imageVector = CompoundIcons.Polls(), + contentDescription = stringResource(id = CommonStrings.a11y_poll), + modifier = Modifier.size(22.dp) + ) + } + Text( + text = title, + style = ElementTheme.typography.fontBodyLgMedium + ) + } +} + +@PreviewsDayNight +@Composable +internal fun PollTitleViewPreview() = ElementPreview { + PollTitleView( + title = "What is your favorite color?", + isPollEnded = false + ) +} From a4e0428403ce8faa05ec42c1df95719b110f5ca9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 30 Aug 2024 18:28:45 +0200 Subject: [PATCH 08/22] Pinned events : better navigation from ViewInTimeline or permalink --- .../android/appnav/LoggedInFlowNode.kt | 21 ++++++++++++------- .../room/joined/JoinedRoomLoadedFlowNode.kt | 6 +++--- .../messages/api/MessagesEntryPoint.kt | 2 +- .../messages/impl/MessagesFlowNode.kt | 15 +++++++------ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index cc50e19895..86e85d7ae6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -34,8 +34,10 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace +import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom @@ -290,21 +292,24 @@ class LoggedInFlowNode @AssistedInject constructor( coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) } } - override fun onPermalinkClick(data: PermalinkData) { + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { when (data) { is PermalinkData.UserLink -> { // Should not happen (handled by MessagesNode) Timber.e("User link clicked: ${data.userId}.") } is PermalinkData.RoomLink -> { - backstack.push( - NavTarget.Room( - roomIdOrAlias = data.roomIdOrAlias, - serverNames = data.viaParameters, - trigger = JoinedRoom.Trigger.Timeline, - initialElement = RoomNavigationTarget.Messages(data.eventId), - ) + val target = NavTarget.Room( + roomIdOrAlias = data.roomIdOrAlias, + serverNames = data.viaParameters, + trigger = JoinedRoom.Trigger.Timeline, + initialElement = RoomNavigationTarget.Messages(data.eventId), ) + if (pushToBackstack) { + backstack.push(target) + } else { + backstack.singleTop(target) + } } is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index d181b22bed..affead0b4c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -77,7 +77,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( ), DaggerComponentOwner { interface Callback : Plugin { fun onOpenRoom(roomId: RoomId) - fun onPermalinkClick(data: PermalinkData) + fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onForwardedToSingleRoom(roomId: RoomId) fun onOpenGlobalNotificationSettings() } @@ -147,8 +147,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( backstack.push(NavTarget.RoomMemberDetails(userId)) } - override fun onPermalinkClick(data: PermalinkData) { - callbacks.forEach { it.onPermalinkClick(data) } + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { + callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } } override fun onForwardedToSingleRoom(roomId: RoomId) { diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 6bd1045c12..cff060ca42 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -41,7 +41,7 @@ interface MessagesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onRoomDetailsClick() fun onUserDataClick(userId: UserId) - fun onPermalinkClick(data: PermalinkData) + fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean = true) fun onForwardedToSingleRoom(roomId: RoomId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 5472741e02..feecc42661 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -28,7 +28,6 @@ import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -69,6 +68,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -308,16 +308,15 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onViewInTimelineClick(eventId: EventId) { - backstack.newRoot(NavTarget.Messages(overriddenFocusedEventId = eventId)) + val permalinkData = PermalinkData.RoomLink( + roomIdOrAlias = room.roomId.toRoomIdOrAlias(), + eventId = eventId, + ) + callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) } } override fun onRoomPermalinkClick(data: PermalinkData.RoomLink) { - if (room.matches(data.roomIdOrAlias)) { - val eventId = data.eventId - backstack.newRoot(NavTarget.Messages(overriddenFocusedEventId = eventId)) - } else { - callbacks.forEach { it.onPermalinkClick(data) } - } + callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = !room.matches(data.roomIdOrAlias)) } } override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { From e7228b94607e5ae26c7b278ddef00cc03cd659cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Sep 2024 14:06:05 +0200 Subject: [PATCH 09/22] Pinned events : add debounce on list --- .../impl/pinned/banner/PinnedMessagesBannerPresenter.kt | 4 ++-- .../impl/pinned/list/PinnedMessagesListPresenter.kt | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index b577995acb..4c9238da00 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -46,7 +46,7 @@ import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, private val itemFactory: PinnedMessagesBannerItemFactory, - private val timelineController: PinnedEventsTimelineProvider, + private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, ) : Presenter { private val pinnedItems = mutableStateOf>>(AsyncData.Uninitialized) @@ -124,7 +124,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( ) { val updatedOnItemsChange by rememberUpdatedState(onItemsChange) LaunchedEffect(Unit) { - timelineController.timelineStateFlow + pinnedEventsTimelineProvider.timelineStateFlow .flatMapLatest { asyncTimeline -> when (asyncTimeline) { AsyncData.Uninitialized -> flowOf(AsyncData.Uninitialized) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 5e61b4903b..b3fb8b8927 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -50,13 +50,16 @@ import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesListPresenter @AssistedInject constructor( @Assisted private val navigator: PinnedMessagesListNavigator, @@ -171,6 +174,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( } } + @OptIn(FlowPreview::class) @Composable private fun PinnedMessagesListEffect(onItemsChange: (AsyncData>) -> Unit) { val updatedOnItemsChange by rememberUpdatedState(onItemsChange) @@ -183,7 +187,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor( 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 -> + val timelineItemsFlow = asyncTimeline.data.timelineItems.debounce(300.milliseconds) + combine(timelineItemsFlow, room.membersStateFlow) { items, membersState -> timelineItemsFactory.replaceWith( timelineItems = items, roomMembers = membersState.roomMembers().orEmpty() From faae2d1004d33a9b5c8965356ee04e25e514796b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 2 Sep 2024 14:06:23 +0200 Subject: [PATCH 10/22] Pinned messages list : navigation from room details --- .../room/joined/JoinedRoomLoadedFlowNode.kt | 60 ++++++++++++------- features/messages/api/build.gradle.kts | 1 + .../messages/api/MessagesEntryPoint.kt | 20 +++++-- .../pinned/IsPinnedMessagesFeatureEnabled.kt | 24 ++++++++ .../impl/DefaultMessagesEntryPoint.kt | 8 ++- .../messages/impl/MessagesFlowNode.kt | 17 ++---- .../pinned/IsPinnedMessagesFeatureEnabled.kt | 6 +- .../roomdetails/api/RoomDetailsEntryPoint.kt | 3 + features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 32 ++++++++++ .../roomdetails/impl/RoomDetailsNode.kt | 8 +++ .../roomdetails/impl/RoomDetailsPresenter.kt | 7 +++ .../roomdetails/impl/RoomDetailsState.kt | 2 + .../impl/RoomDetailsStateProvider.kt | 4 ++ .../roomdetails/impl/RoomDetailsView.kt | 22 +++++++ 15 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/IsPinnedMessagesFeatureEnabled.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index affead0b4c..7794dab216 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -128,6 +128,14 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { callbacks.forEach { it.onOpenRoom(roomId) } } + + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { + callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } + } + + override fun onForwardedToSingleRoom(roomId: RoomId) { + callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + } } return roomDetailsEntryPoint.nodeBuilder(this, buildContext) .params(RoomDetailsEntryPoint.Params(initialTarget)) @@ -138,27 +146,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.Messages -> { - val callback = object : MessagesEntryPoint.Callback { - override fun onRoomDetailsClick() { - backstack.push(NavTarget.RoomDetails) - } - - override fun onUserDataClick(userId: UserId) { - backstack.push(NavTarget.RoomMemberDetails(userId)) - } - - override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { - callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } - } - - override fun onForwardedToSingleRoom(roomId: RoomId) { - callbacks.forEach { it.onForwardedToSingleRoom(roomId) } - } - } - messagesEntryPoint.nodeBuilder(this, buildContext) - .params(MessagesEntryPoint.Params(navTarget.focusedEventId)) - .callback(callback) - .build() + createMessagesNode(buildContext, navTarget) } NavTarget.RoomDetails -> { createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails) @@ -172,6 +160,36 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( } } + private fun createMessagesNode( + buildContext: BuildContext, + navTarget: NavTarget.Messages, + ): Node { + val callback = object : MessagesEntryPoint.Callback { + override fun onRoomDetailsClick() { + backstack.push(NavTarget.RoomDetails) + } + + override fun onUserDataClick(userId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(userId)) + } + + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { + callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } + } + + override fun onForwardedToSingleRoom(roomId: RoomId) { + callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + } + } + val params = MessagesEntryPoint.Params( + MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId) + ) + return messagesEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() + } + sealed interface NavTarget : Parcelable { @Parcelize data class Messages(val focusedEventId: EventId? = null) : NavTarget diff --git a/features/messages/api/build.gradle.kts b/features/messages/api/build.gradle.kts index 5e15d8f38d..4eff3ebcb5 100644 --- a/features/messages/api/build.gradle.kts +++ b/features/messages/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-compose-library") + id("kotlin-parcelize") } android { diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index cff060ca42..43544998ba 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -16,17 +16,27 @@ package io.element.android.features.messages.api +import android.os.Parcelable import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData +import kotlinx.parcelize.Parcelize interface MessagesEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + sealed interface InitialTarget : Parcelable { + @Parcelize + data class Messages(val focusedEventId: EventId?) : InitialTarget + + @Parcelize + data object PinnedMessages : InitialTarget + } interface NodeBuilder { fun params(params: Params): NodeBuilder @@ -34,14 +44,14 @@ interface MessagesEntryPoint : FeatureEntryPoint { fun build(): Node } - data class Params( - val focusedEventId: EventId?, - ) - interface Callback : Plugin { fun onRoomDetailsClick() fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean = true) fun onForwardedToSingleRoom(roomId: RoomId) } + + data class Params(val initialTarget: InitialTarget) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/IsPinnedMessagesFeatureEnabled.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/IsPinnedMessagesFeatureEnabled.kt new file mode 100644 index 0000000000..829683d89d --- /dev/null +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/IsPinnedMessagesFeatureEnabled.kt @@ -0,0 +1,24 @@ +/* + * 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.api.pinned + +import androidx.compose.runtime.Composable + +fun interface IsPinnedMessagesFeatureEnabled { + @Composable + operator fun invoke(): Boolean +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index 0f6a6358d3..73991cfda5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -32,7 +32,7 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { return object : MessagesEntryPoint.NodeBuilder { override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { - plugins += MessagesFlowNode.Inputs(focusedEventId = params.focusedEventId) + plugins += MessagesEntryPoint.Params(params.initialTarget) return this } @@ -47,3 +47,9 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { } } } + + +internal fun MessagesEntryPoint.InitialTarget.toNavTarget() = when (this) { + is MessagesEntryPoint.InitialTarget.Messages -> MessagesFlowNode.NavTarget.Messages(focusedEventId) + MessagesEntryPoint.InitialTarget.PinnedMessages -> MessagesFlowNode.NavTarget.PinnedMessagesList +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index feecc42661..9e32655927 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -105,7 +105,7 @@ class MessagesFlowNode @AssistedInject constructor( private val timelineController: TimelineController, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Messages(overriddenFocusedEventId = null), + initialElement = plugins.filterIsInstance().first().initialTarget.toNavTarget(), savedStateMap = buildContext.savedStateMap, ), overlay = Overlay( @@ -114,16 +114,13 @@ class MessagesFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins ) { - data class Inputs(val focusedEventId: EventId?) : NodeInputs - - private val inputs = inputs() sealed interface NavTarget : Parcelable { @Parcelize data object Empty : NavTarget @Parcelize - data class Messages(val overriddenFocusedEventId: EventId?) : NavTarget + data class Messages(val focusedEventId: EventId?) : NavTarget @Parcelize data class MediaViewer( @@ -157,7 +154,7 @@ class MessagesFlowNode @AssistedInject constructor( data class EditPoll(val eventId: EventId) : NavTarget @Parcelize - data object PinnedEvents : NavTarget + data object PinnedMessagesList : NavTarget } private val callbacks = plugins() @@ -236,12 +233,10 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onViewAllPinnedEvents() { - backstack.push(NavTarget.PinnedEvents) + backstack.push(NavTarget.PinnedMessagesList) } } - val inputs = MessagesNode.Inputs( - focusedEventId = navTarget.overriddenFocusedEventId ?: inputs.focusedEventId, - ) + val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId) createNode(buildContext, listOf(callback, inputs)) } is NavTarget.MediaViewer -> { @@ -297,7 +292,7 @@ class MessagesFlowNode @AssistedInject constructor( .params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.eventId))) .build() } - NavTarget.PinnedEvents -> { + NavTarget.PinnedMessagesList -> { val callback = object : PinnedMessagesListNode.Callback { override fun onEventClick(event: TimelineItem.Event) { processEventClick(event) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt index 5ef5e2c793..71ae30c1a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled import io.element.android.libraries.di.AppScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags @@ -30,11 +31,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject -fun interface IsPinnedMessagesFeatureEnabled { - @Composable - operator fun invoke(): Boolean -} - @ContributesBinding(AppScope::class) class DefaultIsPinnedMessagesFeatureEnabled @Inject constructor( private val featureFlagService: FeatureFlagService, diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index bde560189b..7b027849c4 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkData import kotlinx.parcelize.Parcelize interface RoomDetailsEntryPoint : FeatureEntryPoint { @@ -43,6 +44,8 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenGlobalNotificationSettings() fun onOpenRoom(roomId: RoomId) + fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) + fun onForwardedToSingleRoom(roomId: RoomId) } interface NodeBuilder { diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 07302b4876..def1e0d2d5 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.features.userprofile.shared) implementation(projects.services.analytics.api) implementation(projects.features.poll.api) + implementation(projects.features.messages.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 4a34d64ad0..0ab0608566 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -31,6 +31,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.anvilannotations.ContributesNode import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.poll.api.history.PollHistoryEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode @@ -49,6 +50,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode @@ -64,6 +66,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( private val elementCallEntryPoint: ElementCallEntryPoint, private val room: MatrixRoom, private val analyticsService: AnalyticsService, + private val messagesEntryPoint: MessagesEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -105,6 +108,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object AdminSettings : NavTarget + + @Parcelize + data object PinnedMessagesList : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -139,6 +145,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( backstack.push(NavTarget.AdminSettings) } + override fun openPinnedMessagesList() { + backstack.push(NavTarget.PinnedMessagesList) + } + override fun onJoinCall() { val inputs = CallType.RoomCall( sessionId = room.sessionId, @@ -224,6 +234,28 @@ class RoomDetailsFlowNode @AssistedInject constructor( is NavTarget.AdminSettings -> { createNode(buildContext) } + NavTarget.PinnedMessagesList -> { + val params = MessagesEntryPoint.Params( + MessagesEntryPoint.InitialTarget.PinnedMessages + ) + val callback = object : MessagesEntryPoint.Callback { + override fun onRoomDetailsClick() = Unit + + override fun onUserDataClick(userId: UserId) = Unit + + override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { + plugins().forEach { it.onPermalinkClick(data, pushToBackstack) } + } + + override fun onForwardedToSingleRoom(roomId: RoomId) { + plugins().forEach { it.onForwardedToSingleRoom(roomId) } + } + } + return messagesEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index d55966315e..4c726ab194 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -55,6 +55,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openAvatarPreview(name: String, url: String) fun openPollHistory() fun openAdminSettings() + fun openPinnedMessagesList() fun onJoinCall() } @@ -115,6 +116,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openAdminSettings() } } + private fun openPinnedMessages() { + callbacks.forEach { it.openPinnedMessagesList() } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -144,6 +149,9 @@ class RoomDetailsNode @AssistedInject constructor( openPollHistory = ::openPollHistory, openAdminSettings = this::openAdminSettings, onJoinCallClick = ::onJoinCall, + onPinnedMessagesClick = ::openPinnedMessages ) } + + } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 163e466e1f..e086e15e42 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse @@ -67,6 +68,7 @@ class RoomDetailsPresenter @Inject constructor( private val leaveRoomPresenter: LeaveRoomPresenter, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, + private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, ) : Presenter { @Composable override fun present(): RoomDetailsState { @@ -83,6 +85,9 @@ class RoomDetailsPresenter @Inject constructor( val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } val isPublic by remember { derivedStateOf { roomInfo?.isPublic.orFalse() } } + val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() + val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size ?: 0 } } + LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) if (canShowNotificationSettings.value) { @@ -156,6 +161,8 @@ class RoomDetailsPresenter @Inject constructor( displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, isPublic = isPublic, heroes = roomInfo?.heroes.orEmpty().toPersistentList(), + canShowPinnedMessages = canShowPinnedMessages, + pinnedMessagesCount = pinnedMessagesCount, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index f6a63d70f5..0d3578db1d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -46,6 +46,8 @@ data class RoomDetailsState( val displayRolesAndPermissionsSettings: Boolean, val isPublic: Boolean, val heroes: ImmutableList, + val canShowPinnedMessages: Boolean, + val pinnedMessagesCount: Int, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 45bdf3163d..f6cbebaf7c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -105,6 +105,8 @@ fun aRoomDetailsState( displayAdminSettings: Boolean = false, isPublic: Boolean = true, heroes: List = emptyList(), + canShowPinnedMessages: Boolean = true, + pinnedMessagesCount: Int = 3, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -126,6 +128,8 @@ fun aRoomDetailsState( displayRolesAndPermissionsSettings = displayAdminSettings, isPublic = isPublic, heroes = heroes.toPersistentList(), + canShowPinnedMessages = canShowPinnedMessages, + pinnedMessagesCount = pinnedMessagesCount, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index fc2a051dc6..2ef9e0718e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -103,6 +103,7 @@ fun RoomDetailsView( openPollHistory: () -> Unit, openAdminSettings: () -> Unit, onJoinCallClick: () -> Unit, + onPinnedMessagesClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -183,6 +184,13 @@ fun RoomDetailsView( } ) + if(state.canShowPinnedMessages) { + PinnedMessagesItem( + pinnedMessagesCount = state.pinnedMessagesCount, + onPinnedMessagesClick = onPinnedMessagesClick + ) + } + if (state.displayRolesAndPermissionsSettings) { ListItem( headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) }, @@ -503,6 +511,19 @@ private fun MembersItem( ) } +@Composable +private fun PinnedMessagesItem( + pinnedMessagesCount: Int, + onPinnedMessagesClick: () -> Unit, +) { + ListItem( + headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())), + trailingContent = ListItemContent.Text(pinnedMessagesCount.toString()), + onClick = onPinnedMessagesClick, + ) +} + @Composable private fun PollsSection( openPollHistory: () -> Unit, @@ -573,5 +594,6 @@ private fun ContentToPreview(state: RoomDetailsState) { openPollHistory = {}, openAdminSettings = {}, onJoinCallClick = {}, + onPinnedMessagesClick = {}, ) } From 80f750ae344b294f7db27245a1ab20d96d4b6036 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Sep 2024 14:36:35 +0200 Subject: [PATCH 11/22] Pinned messages list : add loading for pinned messages count and add tests --- .../roomdetails/impl/RoomDetailsPresenter.kt | 2 +- .../roomdetails/impl/RoomDetailsState.kt | 2 +- .../impl/RoomDetailsStateProvider.kt | 3 ++- .../roomdetails/impl/RoomDetailsView.kt | 15 ++++++++++++--- .../roomdetails/RoomDetailsPresenterTest.kt | 10 +++++++--- .../roomdetails/impl/RoomDetailsViewTest.kt | 18 ++++++++++++++++++ 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index e086e15e42..fe15beb983 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -86,7 +86,7 @@ class RoomDetailsPresenter @Inject constructor( val isPublic by remember { derivedStateOf { roomInfo?.isPublic.orFalse() } } val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() - val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size ?: 0 } } + val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } } LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 0d3578db1d..afce4f879a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -47,7 +47,7 @@ data class RoomDetailsState( val isPublic: Boolean, val heroes: ImmutableList, val canShowPinnedMessages: Boolean, - val pinnedMessagesCount: Int, + val pinnedMessagesCount: Int?, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index f6cbebaf7c..5c881787e4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -53,6 +53,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState(canCall = false, canInvite = false), aRoomDetailsState(isPublic = false), aRoomDetailsState(heroes = aMatrixUserList()), + aRoomDetailsState(pinnedMessagesCount = 3), // Add other state here ) } @@ -106,7 +107,7 @@ fun aRoomDetailsState( isPublic: Boolean = true, heroes: List = emptyList(), canShowPinnedMessages: Boolean = true, - pinnedMessagesCount: Int = 3, + pinnedMessagesCount: Int? = null, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 2ef9e0718e..a0519aa3fa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -66,6 +67,7 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem import io.element.android.libraries.designsystem.theme.components.Icon @@ -184,7 +186,7 @@ fun RoomDetailsView( } ) - if(state.canShowPinnedMessages) { + if (state.canShowPinnedMessages) { PinnedMessagesItem( pinnedMessagesCount = state.pinnedMessagesCount, onPinnedMessagesClick = onPinnedMessagesClick @@ -513,13 +515,20 @@ private fun MembersItem( @Composable private fun PinnedMessagesItem( - pinnedMessagesCount: Int, + pinnedMessagesCount: Int?, onPinnedMessagesClick: () -> Unit, ) { ListItem( headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())), - trailingContent = ListItemContent.Text(pinnedMessagesCount.toString()), + trailingContent = + if (pinnedMessagesCount == null) { + ListItemContent.Custom { + CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(24.dp)) + } + } else { + ListItemContent.Text(pinnedMessagesCount.toString()) + }, onClick = onPinnedMessagesClick, ) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt index ff89aba9be..d2f01498c3 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt @@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -81,6 +82,7 @@ class RoomDetailsPresenterTest { dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), analyticsService: AnalyticsService = FakeAnalyticsService(), + isPinnedMessagesFeatureEnabled: Boolean = true, ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { @@ -99,6 +101,7 @@ class RoomDetailsPresenterTest { roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, leaveRoomPresenter = leaveRoomPresenter, dispatchers = dispatchers, + isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled }, analyticsService = analyticsService, ) } @@ -127,14 +130,15 @@ class RoomDetailsPresenterTest { assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount) assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) - + assertThat(initialState.canShowPinnedMessages).isTrue() + assertThat(initialState.pinnedMessagesCount).isNull() cancelAndIgnoreRemainingEvents() } } @Test fun `present - initial state is updated with roomInfo if it exists`() = runTest { - val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg") + val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg", pinnedEventIds = listOf(AN_EVENT_ID)) val room = aMatrixRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -149,7 +153,7 @@ class RoomDetailsPresenterTest { assertThat(updatedState.roomName).isEqualTo(roomInfo.name) assertThat(updatedState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) assertThat(updatedState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(roomInfo.topic!!)) - + assertThat(updatedState.pinnedMessagesCount).isEqualTo(roomInfo.pinnedEventIds.size) cancelAndIgnoreRemainingEvents() } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index b3bbb21cf6..a5a2f7f18a 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -127,6 +127,22 @@ class RoomDetailsViewTest { } } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on pinned messages invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + canInvite = true, + ), + onPinnedMessagesClick = callback, + ) + rule.clickOn(CommonStrings.screen_room_details_pinned_events_row_title) + } + } + @Config(qualifiers = "h1024dp") @Test fun `click on add topic emit expected event`() { @@ -263,6 +279,7 @@ private fun AndroidComposeTestRule.setRoomD openPollHistory: () -> Unit = EnsureNeverCalled(), openAdminSettings: () -> Unit = EnsureNeverCalled(), onJoinCallClick: () -> Unit = EnsureNeverCalled(), + onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomDetailsView( @@ -277,6 +294,7 @@ private fun AndroidComposeTestRule.setRoomD openPollHistory = openPollHistory, openAdminSettings = openAdminSettings, onJoinCallClick = onJoinCallClick, + onPinnedMessagesClick = onPinnedMessagesClick, ) } } From 2c41d4583aa52019123b1ffdd6c609ea3d3c83be Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 3 Sep 2024 18:21:42 +0200 Subject: [PATCH 12/22] Pinned messages list : fix and add tests --- .../impl/actionlist/ActionListPresenter.kt | 25 ++- .../pinned/list/PinnedMessagesListEvents.kt | 1 - .../list/PinnedMessagesListPresenter.kt | 1 - .../impl/timeline/TimelinePresenter.kt | 1 - .../messages/impl/timeline/TimelineState.kt | 1 - .../impl/timeline/TimelineStateProvider.kt | 2 - .../messages/impl/MessagesPresenterTest.kt | 11 +- .../actionlist/ActionListPresenterTest.kt | 11 +- .../actionlist/FakeActionListPresenter.kt | 34 ++++ .../PinnedMessagesBannerPresenterTest.kt | 27 ++- .../list/FakePinnedMessagesListNavigator.kt | 38 ++++ .../list/PinnedMessagesListPresenterTest.kt | 174 ++++++++++++++++++ 12 files changed, 293 insertions(+), 33 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 1f94d2a847..cbd6c6c789 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -23,9 +23,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor @@ -40,8 +42,7 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded import io.element.android.features.messages.impl.timeline.model.event.canReact import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -52,16 +53,24 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -class ActionListPresenter @AssistedInject constructor( +interface ActionListPresenter : Presenter { + interface Factory { + fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter + } +} + +class DefaultActionListPresenter @AssistedInject constructor( @Assisted private val postProcessor: TimelineItemActionPostProcessor, private val appPreferencesStore: AppPreferencesStore, - private val featureFlagsService: FeatureFlagService, + private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, private val room: MatrixRoom, -) : Presenter { +) : ActionListPresenter { + @AssistedFactory - interface Factory { - fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter + @ContributesBinding(RoomScope::class) + interface Factory: ActionListPresenter.Factory { + override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter } @Composable @@ -73,7 +82,7 @@ class ActionListPresenter @AssistedInject constructor( } val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false) - val isPinnedEventsEnabled by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) + val isPinnedEventsEnabled = isPinnedMessagesFeatureEnabled() val pinnedEventIds by remember { room.roomInfoFlow.map { it.pinnedEventIds } }.collectAsState(initial = persistentListOf()) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt index 54149fd260..e785e3c8ae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvents.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl.pinned.list -import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index b3fb8b8927..471f8d58ab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -83,7 +83,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor( TimelineRoomInfo( isDm = room.isDm, name = room.displayName, - isMainTimeline = false, // We don't need to compute those values userHasPermissionToSendMessage = false, userHasPermissionToSendReaction = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 5324ef44fa..26954d2a63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -222,7 +222,6 @@ class TimelinePresenter @AssistedInject constructor( userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, isCallOngoing = roomInfo?.hasRoomCall.orFalse(), - isMainTimeline = true ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 114dc08a98..d2a66a2e1c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -75,5 +75,4 @@ data class TimelineRoomInfo( val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendReaction: Boolean, val isCallOngoing: Boolean, - val isMainTimeline: Boolean, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index d697eb5554..a1d4524ee2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -242,12 +242,10 @@ internal fun aTimelineRoomInfo( name: String = "Room name", isDm: Boolean = false, userHasPermissionToSendMessage: Boolean = true, - isMainTimeline: Boolean = true, ) = TimelineRoomInfo( isDm = isDm, name = name, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = true, isCallOngoing = false, - isMainTimeline = isMainTimeline, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index c9700a60bd..ce8c254868 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -24,7 +24,10 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState +import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter +import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory @@ -60,6 +63,7 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -1052,11 +1056,6 @@ class MessagesPresenterTest { } } val featureFlagService = FakeFeatureFlagService() - val actionListPresenter = ActionListPresenter( - appPreferencesStore = appPreferencesStore, - featureFlagsService = featureFlagService, - room = matrixRoom, - ) val typingNotificationPresenter = TypingNotificationPresenter( room = matrixRoom, sessionPreferencesStore = sessionPreferencesStore, @@ -1072,7 +1071,7 @@ class MessagesPresenterTest { voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, typingNotificationPresenter = typingNotificationPresenter, - actionListPresenter = actionListPresenter, + actionListPresenterFactory = FakeActionListPresenter.Factory, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index e8ea7cbbc2..720bd54125 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.aUserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent @@ -974,14 +975,10 @@ private fun createActionListPresenter( room: MatrixRoom = FakeMatrixRoom(), ): ActionListPresenter { val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) - val featureFlagsService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.PinnedEvents.key to isPinFeatureEnabled, - ) - ) - return ActionListPresenter( + return DefaultActionListPresenter( + postProcessor = TimelineItemActionPostProcessor.Default, appPreferencesStore = preferencesStore, - featureFlagsService = featureFlagsService, + isPinnedMessagesFeatureEnabled = {isPinFeatureEnabled}, room = room ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt new file mode 100644 index 0000000000..9e34c19567 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt @@ -0,0 +1,34 @@ +/* + * 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.actionlist + +import androidx.compose.runtime.Composable +import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor + +class FakeActionListPresenter : ActionListPresenter { + + object Factory : ActionListPresenter.Factory { + override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { + return FakeActionListPresenter() + } + } + + @Composable + override fun present(): ActionListState { + return anActionListState() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index 1ae4729a40..8b79ecb13b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -17,9 +17,12 @@ package io.element.android.features.messages.impl.pinned.banner import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -31,8 +34,12 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -65,7 +72,7 @@ class PinnedMessagesBannerPresenterTest { } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { - skipItems(1) + skipItems(2) val loadingState = awaitItem() assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) @@ -96,7 +103,7 @@ class PinnedMessagesBannerPresenterTest { } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { - skipItems(2) + skipItems(3) val loadedState = awaitItem() as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1) @@ -135,7 +142,7 @@ class PinnedMessagesBannerPresenterTest { } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { - skipItems(2) + skipItems(3) awaitItem().also { loadedState -> loadedState as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1) @@ -170,7 +177,7 @@ class PinnedMessagesBannerPresenterTest { } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { - skipItems(1) + skipItems(2) awaitItem().also { loadingState -> assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) @@ -193,11 +200,19 @@ class PinnedMessagesBannerPresenterTest { networkMonitor: NetworkMonitor = FakeNetworkMonitor(), isFeatureEnabled: Boolean = true, ): PinnedMessagesBannerPresenter { + val timelineProvider = PinnedEventsTimelineProvider( + room = room, + networkMonitor = networkMonitor, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) + ) + ) + timelineProvider.launchIn(backgroundScope) + return PinnedMessagesBannerPresenter( room = room, itemFactory = itemFactory, - isFeatureEnabled = { isFeatureEnabled }, - networkMonitor = networkMonitor, + pinnedEventsTimelineProvider = timelineProvider, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt new file mode 100644 index 0000000000..5d5f27a97a --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt @@ -0,0 +1,38 @@ +/* + * 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.list + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo + +class FakePinnedMessagesListNavigator: PinnedMessagesListNavigator { + + var onViewInTimelineClickLambda: ((EventId) -> Unit)? = null + override fun onViewInTimelineClick(eventId: EventId) { + onViewInTimelineClickLambda?.invoke(eventId) + } + + var onShowEventDebugInfoClickLambda: ((EventId?, TimelineItemDebugInfo) -> Unit)? = null + override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { + onShowEventDebugInfoClickLambda?.invoke(eventId, debugInfo) + } + + var onForwardEventClickLambda: ((EventId) -> Unit)? = null + override fun onForwardEventClick(eventId: EventId) { + onForwardEventClickLambda?.invoke(eventId) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt new file mode 100644 index 0000000000..d3bf7efa24 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -0,0 +1,174 @@ +/* + * 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.list + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter +import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory +import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.aMessageContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PinnedMessagesListPresenterTest { + + @Test + fun `present - initial state feature disabled`() = runTest { + val room = FakeMatrixRoom( + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ) + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = false) + presenter.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(PinnedMessagesListState.Loading) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - initial state feature enabled`() = runTest { + val room = FakeMatrixRoom( + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(PinnedMessagesListState.Loading) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - timeline failure state`() = runTest { + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.failure(RuntimeException()) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val failureState = awaitItem() + assertThat(failureState).isEqualTo(PinnedMessagesListState.Failed) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - empty state`() = runTest { + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(FakeTimeline()) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf())) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val emptyState = awaitItem() + assertThat(emptyState).isEqualTo(PinnedMessagesListState.Empty) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - filled state`() = runTest { + val messageContent = aMessageContent("A message") + val pinnedEventsTimeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = "FAKE_UNIQUE_ID", + event = anEventTimelineItem( + eventId = AN_EVENT_ID, + content = messageContent, + ), + ) + ) + ) + ) + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + assertThat(filledState.timelineItems).hasSize(1) + assertThat(filledState.loadedPinnedMessagesCount).isEqualTo(1) + assertThat(filledState.userEventPermissions.canRedactOwn).isTrue() + assertThat(filledState.userEventPermissions.canRedactOther).isTrue() + assertThat(filledState.userEventPermissions.canPinUnpin).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + private fun TestScope.createPinnedMessagesListPresenter( + navigator: PinnedMessagesListNavigator = FakePinnedMessagesListNavigator(), + room: MatrixRoom = FakeMatrixRoom(), + networkMonitor: NetworkMonitor = FakeNetworkMonitor(), + isFeatureEnabled: Boolean = true, + ): PinnedMessagesListPresenter { + val timelineProvider = PinnedEventsTimelineProvider( + room = room, + networkMonitor = networkMonitor, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) + ) + ) + timelineProvider.launchIn(backgroundScope) + return PinnedMessagesListPresenter( + navigator = navigator, + room = room, + timelineItemsFactory = aTimelineItemsFactory(), + timelineProvider = timelineProvider, + snackbarDispatcher = SnackbarDispatcher(), + actionListPresenterFactory = FakeActionListPresenter.Factory, + ) + } +} From b10472bd6ca7d0a7ae7f367e29df9302108b8f00 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 11:11:04 +0200 Subject: [PATCH 13/22] Pinned messages list : add some ui tests --- .../PinnedMessagesTimelineListProvider.kt | 9 +- .../pinned/list/PinnedMessagesListViewTest.kt | 121 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt index fe336d8e37..ab8dd48611 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -21,8 +21,10 @@ import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import kotlinx.collections.immutable.toImmutableList open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider { @@ -31,7 +33,9 @@ open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider = emptyList(), actionListState: ActionListState = anActionListState(), aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT, + eventSink: (PinnedMessagesListEvents) -> Unit = {} ) = PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems.toImmutableList(), actionListState = actionListState, userEventPermissions = aUserEventPermissions, - eventSink = {}, + eventSink = eventSink, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt new file mode 100644 index 0000000000..d446e8514b --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -0,0 +1,121 @@ +/* + * 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.list + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.messages.impl.actionlist.ActionListEvents +import io.element.android.features.messages.impl.actionlist.anActionListState +import io.element.android.features.messages.impl.timeline.aTimelineItemList +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PinnedMessagesListViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back calls the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aLoadedPinnedMessagesListState( + eventSink = eventsRecorder + ) + ensureCalledOnce { callback -> + rule.setPinnedMessagesListView( + state = state, + onBackClick = callback + ) + rule.pressBack() + } + } + + @Test + fun `click on an event calls the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val content = aTimelineItemFileContent() + val state = aLoadedPinnedMessagesListState( + timelineItems = aTimelineItemList(content), + eventSink = eventsRecorder + ) + + val event = state.timelineItems.first() as TimelineItem.Event + ensureCalledOnceWithParam(event) { callback -> + rule.setPinnedMessagesListView( + state = state, + onEventClick = callback + ) + rule.onAllNodesWithText(content.body).onFirst().performClick() + } + } + + @Test + fun `long click on an event emits the expected event`() { + val eventsRecorder = EventsRecorder(expectEvents = true) + val content = aTimelineItemFileContent() + val state = aLoadedPinnedMessagesListState( + timelineItems = aTimelineItemList(content), + actionListState = anActionListState(eventSink = eventsRecorder) + ) + + rule.setPinnedMessagesListView( + state = state, + ) + rule.onAllNodesWithText(content.body).onFirst() + .performTouchInput { + longClick() + } + val event = state.timelineItems.first() as TimelineItem.Event + eventsRecorder.assertSingle(ActionListEvents.ComputeForMessage(event, state.userEventPermissions)) + } +} + +private fun AndroidComposeTestRule.setPinnedMessagesListView( + state: PinnedMessagesListState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + PinnedMessagesListView( + state = state, + onBackClick = onBackClick, + onEventClick = onEventClick, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + ) + } +} From 82d19fd65ec89fbe49d7c5ca7164d68443f3de29 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 11:42:04 +0200 Subject: [PATCH 14/22] Pinned messages list : handle error state (shouldn't really happen) --- .../pinned/list/PinnedMessagesListView.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index 9ccc0ab767..b8d2bb4991 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.pollcontent.PollTitleView 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.dialogs.ErrorDialog import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -73,6 +74,7 @@ fun PinnedMessagesListView( onEventClick = onEventClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onErrorDismiss = onBackClick, modifier = Modifier .padding(padding) .consumeWindowInsets(padding), @@ -106,11 +108,18 @@ private fun PinnedMessagesListContent( onEventClick: (event: TimelineItem.Event) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, + onErrorDismiss: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier.fillMaxSize()) { when (state) { - PinnedMessagesListState.Failed -> Unit + PinnedMessagesListState.Failed -> { + ErrorDialog( + title = stringResource(id = CommonStrings.error_unknown), + content = stringResource(id = CommonStrings.error_failed_loading_messages), + onDismiss = onErrorDismiss + ) + } PinnedMessagesListState.Empty -> PinnedMessagesListEmpty() is PinnedMessagesListState.Filled -> PinnedMessagesListLoaded( state = state, @@ -133,16 +142,16 @@ private fun PinnedMessagesListEmpty( ) { Box( modifier = modifier.padding( - horizontal = 16.dp, - vertical = 48.dp - ) + horizontal = 32.dp, + vertical = 48.dp, + ), + contentAlignment = Alignment.Center, ) { 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, ) } } From 71e13b2aa20b9a51809a7c19e25d45e3715e653d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 11:42:54 +0200 Subject: [PATCH 15/22] Pinned messages : add comment on PinnedEventsTimelineProvider --- .../messages/impl/pinned/PinnedEventsTimelineProvider.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt index 1e26e03600..d3f6254c3e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -56,7 +56,10 @@ class PinnedEventsTimelineProvider @Inject constructor( combine( featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents), networkMonitor.connectivity - ) { isEnabled, _ -> isEnabled } + ) { + // do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed + isEnabled, _ -> isEnabled + } .onEach { isFeatureEnabled -> if (isFeatureEnabled) { loadTimelineIfNeeded() From 5d18b1a8fa6284932bdd89e018636929cd6005e0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 12:09:17 +0200 Subject: [PATCH 16/22] Pinned messages list : fix all tests after changes --- ...ider.kt => PinnedMessagesListStateProvider.kt} | 2 +- ...nnedMessagesListTimelineActionPostProcessor.kt | 1 - .../impl/pinned/list/PinnedMessagesListView.kt | 2 +- .../messages/impl/MessagesPresenterTest.kt | 1 - .../poll/impl/history/PollHistoryPresenterTest.kt | 3 +-- .../RoomBeginningPostProcessorTest.kt | 15 ++++++++------- 6 files changed, 11 insertions(+), 13 deletions(-) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/{PinnedMessagesTimelineListProvider.kt => PinnedMessagesListStateProvider.kt} (96%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt index ab8dd48611..c98d3d0b75 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt @@ -27,7 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import kotlinx.collections.immutable.toImmutableList -open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider { +open class PinnedMessagesListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aFailedPinnedMessagesListState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt index da6e13aac2..78279cf3d9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt @@ -30,7 +30,6 @@ object PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostPro private fun predicate(action: TimelineItemAction): Boolean { return when (action) { - is TimelineItemAction.Pin, is TimelineItemAction.Unpin, is TimelineItemAction.Redact, is TimelineItemAction.Forward, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index b8d2bb4991..754419ea57 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -260,7 +260,7 @@ private fun TimelineItemEventContentViewWrapper( @PreviewsDayNight @Composable -fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineStateProvider::class) state: PinnedMessagesListState) = +internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListStateProvider::class) state: PinnedMessagesListState) = ElementPreview { PinnedMessagesListView( state = state, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index ce8c254868..78f28a36c5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -999,7 +999,6 @@ class MessagesPresenterTest { ): MessagesPresenter { val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) - val appPreferencesStore = InMemoryAppPreferencesStore() val sessionPreferencesStore = InMemorySessionPreferencesStore() val mentionSpanProvider = MentionSpanProvider(FakePermalinkParser()) val messageComposerPresenter = MessageComposerPresenter( diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index 652aafef4f..302f903a2d 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -38,7 +38,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -180,7 +179,7 @@ class PollHistoryPresenterTest { sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction, pollHistoryItemFactory = pollHistoryItemFactory, - timelineProvider = LiveTimelineProvider(room), + room = room, ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index 9700b70a77..72622f4b0a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent @@ -36,7 +37,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEmpty() } @@ -53,7 +54,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(expected) } @@ -64,7 +65,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo( listOf(processor.createRoomBeginningItem()) + timelineItems @@ -76,7 +77,7 @@ class RoomBeginningPostProcessorTest { val timelineItems = listOf( MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(timelineItems) } @@ -87,7 +88,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -97,7 +98,7 @@ class RoomBeginningPostProcessorTest { val timelineItems = listOf( MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @@ -108,7 +109,7 @@ class RoomBeginningPostProcessorTest { MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))), ) - val processor = RoomBeginningPostProcessor(mode) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } From 8c87272a83203369249d2e9b7c74640fb37de695 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 4 Sep 2024 12:22:08 +0000 Subject: [PATCH 17/22] Update screenshots --- ...sages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png | 3 +++ ...sages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png | 3 +++ ...sages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png | 3 +++ ...sages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png | 3 +++ ...ges.impl.pinned.list_PinnedMessagesListView_Night_0_en.png | 3 +++ ...ges.impl.pinned.list_PinnedMessagesListView_Night_1_en.png | 3 +++ ...ges.impl.pinned.list_PinnedMessagesListView_Night_2_en.png | 3 +++ ...ges.impl.pinned.list_PinnedMessagesListView_Night_3_en.png | 3 +++ .../features.poll.api.pollcontent_PollTitleView_Day_0_en.png | 3 +++ ...features.poll.api.pollcontent_PollTitleView_Night_0_en.png | 3 +++ .../images/features.roomdetails.impl_RoomDetailsDark_0_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_10_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_11_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_12_en.png | 4 ++-- .../features.roomdetails.impl_RoomDetailsDark_13_en.png | 3 +++ .../images/features.roomdetails.impl_RoomDetailsDark_1_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_2_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_3_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_4_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_5_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_6_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_7_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_8_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetailsDark_9_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_0_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_10_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_11_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_12_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_13_en.png | 3 +++ .../images/features.roomdetails.impl_RoomDetails_1_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_2_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_3_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_4_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_5_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_6_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_7_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_8_en.png | 4 ++-- .../images/features.roomdetails.impl_RoomDetails_9_en.png | 4 ++-- 38 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png new file mode 100644 index 0000000000..bb1d148b29 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da5fed3d33c60f471b19fcf9761c5feb1e8fff303d84e951ecd53e8cd54613d0 +size 19938 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png new file mode 100644 index 0000000000..df40973190 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fea6dbb47a656e0d39181a40b32277e17f0fe52c99e8c1519d5f09ff9423e98c +size 8983 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png new file mode 100644 index 0000000000..e74640592c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a495cd353317e667d667d3f98c4b844568b0375d83b7cb4091bafd30dec194 +size 31738 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png new file mode 100644 index 0000000000..ba8fae21e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40c712c0518e86ac4fc3980fa9c31d50dc0371f08af4cf27d8d70202b42e3d9a +size 45758 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png new file mode 100644 index 0000000000..0c8f789e7a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93fe898626703045ac2efc2c5c9c2a404a9fa446333649ab77cf1acd7da4f1e7 +size 18057 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png new file mode 100644 index 0000000000..3b2f1ee82b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f221c0af78be8d43a481ff0b122ea0587e0e0618c6f6fec78cb91368e940f701 +size 8720 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png new file mode 100644 index 0000000000..d689a95fc8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97ae5c5a01f94e35d2d05d3b1a6f3be992fcf49f9674bdf913753c5430d3e0bf +size 30749 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png new file mode 100644 index 0000000000..f03d98bdd1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa5d44ab86f523c740caef1e3a409e010dd100429f85ff806952b0a857cd3615 +size 46362 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png new file mode 100644 index 0000000000..a2e90f7926 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73d64e6f6fd0f979c1929f3f1118d26890e55c301031dd3ca0f64647429dd42c +size 9358 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png new file mode 100644 index 0000000000..dac28412c2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5b23e028c2df95e19ee96b0715b3da7f32ea15d7b50a6bd6068add0263c0c2a +size 9081 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png index f3becc3f46..d5a8b351c4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2e96d71fd2e0e67415ab5d69b2712661f4e453980e202e60717cc73e1e3fa81 -size 45821 +oid sha256:8cef0a991d7b6a66dac0687e278231dcd4c9084e3ea04fc5e28d7d15f8b30795 +size 40901 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png index 9ad15d62b1..0d46da4eb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1884020edf5182e4be0ef7408ae09d3ebdc79e4f482c0ed22c47864fb4ca3b6d -size 43682 +oid sha256:1481831145c4a32c046f63748a1ac2eccfe8ff954b1926b22115110e9f4c2e44 +size 45041 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png index 17e9510513..e25d3bb89e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50940f08a015ca4ecbd4f88810b183d1632daa34e8f79c38017f20ad5a1ce90a -size 42637 +oid sha256:75d9e78aec05eacb67872f036490d2dbed6174e160bfc1f983123e043c00d056 +size 44003 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png index 9ca537357d..2702d14999 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:259aced4d75ec473abf8f4fb41c3c734e69618a552b2cf0b9ceeb98d59deec25 -size 45717 +oid sha256:0a6a3a952c2b2e5304cd83674fb209d11f07a366375aec36cc4e4f981a541b03 +size 47082 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png new file mode 100644 index 0000000000..231dd5fe5d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ed5843b61db2188b6673a07f899966ed041cd5acfe12dcf40539d96e77eaab +size 45455 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png index e42f829f62..2aca438058 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4885f85102e8e8a509586fb48e7a26d5ceb1dd7aa6897b955cb781eed87b9f30 -size 35023 +oid sha256:85e674e93c73ddbe77c71bfa8ca6f117b9f0d6863fa5137f12f0e9ed62a6e4b7 +size 37414 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png index b262ff6214..9d0de360ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6d8d1c7aa92f526398221aeff77cc44ebdad36bc4a1259ebb3c9635483af1f5 -size 37117 +oid sha256:de1debdc3ff7a00a7de3b7b381a5bd1102016d39e072af2325c4ffb40988d054 +size 38281 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png index e44dff5529..54109a1d99 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3679bf87660a8bd799ae9c4397266227a0f46559bdcae6ddecec0930d5c78e95 -size 35993 +oid sha256:86a5148af36166fd0cb8fb022b85645e64f0cccb9760848d75af4927f715cdc6 +size 38413 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png index 38cccf98e2..ea0e3a147d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae27c470cb9be1fa75b85b607bcc3666839801376e00b9c0ec8b86d229d907b -size 43236 +oid sha256:9893ea709177f3f0101bfaed241de44e85d179d99f133f6d4057a251b88c3a9d +size 44234 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png index ce9eec3248..f926d67237 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea6ba2ec613561a26331c72ca89a6bd44b96c160bee75bc9a9b9a06339426275 -size 41660 +oid sha256:c1ca7803057968031f909b1311b023bfb9ae1978003c8beb80a85d065968bd23 +size 42324 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png index ce9eec3248..f926d67237 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea6ba2ec613561a26331c72ca89a6bd44b96c160bee75bc9a9b9a06339426275 -size 41660 +oid sha256:c1ca7803057968031f909b1311b023bfb9ae1978003c8beb80a85d065968bd23 +size 42324 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png index 308b3bc8f1..7b67afcccb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9ea4e85c5dc2d9d5c609b29e6232a5932c9b3bee81c459e9214ac16f7a3764 -size 44986 +oid sha256:cfcee02543f55d395112fd662d785ef553260a4457b5ab753b6198c0c13a2068 +size 46345 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png index c2416c95b3..61b685445c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:569a0dfdc4c5c77f44999b0e9e7a1b2458cac6b01dfd56800d3e7c3b5f8c0347 -size 43930 +oid sha256:50023e5cd9260b3db79fa1ab15489e165dc7df9663584317d0fe2243f1622ea2 +size 45288 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png index 12c112d879..b9ef9dfb9a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45e0e9952c3ca0fb805b5e53b990c3ec54732a971ecc658e25ef73254b3f1ca4 -size 43871 +oid sha256:487ecdc6e7bbb2389e8f21a99b348939f371c6fa8fc090fcff46f5a51e2390cb +size 45230 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png index fea3ce3463..025e87bcc1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcf1f93aa20a0acd4c32f761fdd39abf46016c84072f0a22c9b9cf0dd7f2c6f2 -size 46788 +oid sha256:38f6a49907dfbafdeb18184ea70315882f26729353975748be33246dd06de810 +size 41776 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png index 42fce9b5d6..c445743cf2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc8fb59e77e8e09e79f5d89e039f302f28e2451faa025e9f1ce3b0ac48f75f8d -size 44404 +oid sha256:13189861bd0b613aabe126a7064277695f9e0a31ce6e7d0104c1b786900d855c +size 45869 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png index 661f82e060..d11d9a6363 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3166a62b24bfaf55d3dfa5d6e9fcd6dca2ee2789928efd66d8147e4e11d0ce0 -size 43377 +oid sha256:f6231ddd26d71291a3acfe347ccaa5b5213809969d3a21667f5c872da145bbe6 +size 44844 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png index a71a604612..76b488e522 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f799d30b5c3d847cc8011ce4aefbb8710b34df8f11116842af035b05d233cfa1 -size 46485 +oid sha256:f4841522e54ab49cf7f2ac4e06223addf050c4a0f9c7a3e351d73fbd0fbb62b9 +size 47959 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png new file mode 100644 index 0000000000..c221989a49 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f346764da31583076508550c339be523d15f338b00f2f412e1b117bd1e3b5062 +size 46333 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png index 3221ed0029..61e99bf7c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09ce1743e0e3c5629fab6d0c2d06cc15be7229e446673b05f49f0e031a1c2823 -size 35776 +oid sha256:17a68abaa010d037c00b3ae24ab470c9cf2e7b42d1c45391819b5e46e3b3c2c7 +size 38265 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png index 54067184f1..624a512f20 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84ef09f2163f10a583853a52008f5a53f26c90350136a970fd55e0199f5fd47f -size 37907 +oid sha256:3d67989cc90311a6810882f1cc9f63d4849c9768892403c6366f260b6f01782a +size 39097 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png index a5bd17734c..68d3eceab3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9941f55b35128067bd7560ed24d44bc0bbe8e13a6702d6ec2d35304462f755a2 -size 36503 +oid sha256:777cecce6d855418ade591c3f02d97dbc82bc81f3317bc64cc76ae8f0e94a792 +size 39004 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png index 103d53664c..74d9ecb427 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b300a88051346537beac5096895ef87b007e56fdddc33ad63db092f4c813a454 -size 44041 +oid sha256:2fb04b9ecf34bd1f9218b137ad213a385abd03b0a4767f7c88ef04ee496f8e4c +size 45084 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png index 9030577759..de70559d50 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe90dffd26521d58e0956b9ea967c641f75a7cdeb39052abbfcdf85754c5dd3 -size 42281 +oid sha256:31eba128b859c1a5b4da1f62de323d29f62a36dd962877fbf3fdea0a51b81fde +size 42948 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png index 9030577759..de70559d50 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe90dffd26521d58e0956b9ea967c641f75a7cdeb39052abbfcdf85754c5dd3 -size 42281 +oid sha256:31eba128b859c1a5b4da1f62de323d29f62a36dd962877fbf3fdea0a51b81fde +size 42948 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png index 88bdc93f10..3accfb7391 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b3d79ae55b26478c2e6d1910b37a98eb22ac37e9bfd3a33de4dc805e82028b -size 45850 +oid sha256:47260328f01cc55a7825ab8bebe540aac41efcee77cc52a0d82deb3fb1e496b3 +size 47321 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png index 371a43df04..24d346dcdf 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02a966757cc01f8707738d3cd29d05df0bf3842069fec812c7223e2e29da5dbf -size 44761 +oid sha256:9cb0fb71d0e3433bd9512d435f47c3f116627202d1f81debd5f3c43eece64a5d +size 46223 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png index 816e803108..bd5aaf721e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a2148c28660bd9fd3a45b1f06b331966f79c77166b40bb1b7afb5b926407817 -size 44647 +oid sha256:6939bdfecb6177a7c180bf94d18d80aea0ebceb16f06c87d0cc598068bc73765 +size 46114 From ff3646ead00bb41c39a171d16db8b07c5fcfc071 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 14:27:18 +0200 Subject: [PATCH 18/22] Pinned messages: clean code --- .../android/appnav/LoggedInFlowNode.kt | 1 - .../messages/api/MessagesEntryPoint.kt | 1 - .../impl/DefaultMessagesEntryPoint.kt | 1 - .../messages/impl/MessagesFlowNode.kt | 2 - .../features/messages/impl/MessagesNode.kt | 2 - .../impl/actionlist/ActionListPresenter.kt | 3 +- .../actionlist/model/TimelineItemAction.kt | 2 +- .../model/TimelineItemActionPostProcessor.kt | 1 - .../impl/forward/ForwardMessagesNode.kt | 2 - .../impl/forward/ForwardMessagesPresenter.kt | 5 --- ... DefaultIsPinnedMessagesFeatureEnabled.kt} | 0 .../pinned/PinnedEventsTimelineProvider.kt | 3 +- .../pinned/list/PinnedMessagesListNode.kt | 2 - .../list/PinnedMessagesListPresenter.kt | 1 - ...MessagesListTimelineActionPostProcessor.kt | 1 - .../impl/timeline/TimelinePresenter.kt | 1 - .../messages/impl/timeline/TimelineState.kt | 1 - .../impl/timeline/TimelineStateProvider.kt | 1 - .../TimelineItemGroupedEventsRow.kt | 38 ++++++++++--------- .../timeline/components/TimelineItemRow.kt | 19 +++++----- .../factories/TimelineItemsFactory.kt | 1 - .../messages/impl/MessagesPresenterTest.kt | 5 --- .../actionlist/ActionListPresenterTest.kt | 4 +- .../actionlist/FakeActionListPresenter.kt | 1 - .../PinnedMessagesBannerPresenterTest.kt | 4 -- .../list/FakePinnedMessagesListNavigator.kt | 3 +- .../list/PinnedMessagesListPresenterTest.kt | 1 - .../poll/impl/history/PollHistoryPresenter.kt | 1 - .../roomdetails/impl/RoomDetailsNode.kt | 2 - .../roomdetails/impl/RoomDetailsViewTest.kt | 1 - .../core/coroutine/DerivedStateFlow.kt | 1 - 31 files changed, 36 insertions(+), 75 deletions(-) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/{IsPinnedMessagesFeatureEnabled.kt => DefaultIsPinnedMessagesFeatureEnabled.kt} (100%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 6e996c1440..d218d49dee 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -34,7 +34,6 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.singleTop diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 43544998ba..1bd1bfe756 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import kotlinx.parcelize.Parcelize interface MessagesEntryPoint : FeatureEntryPoint { - sealed interface InitialTarget : Parcelable { @Parcelize data class Messages(val focusedEventId: EventId?) : InitialTarget diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index 73991cfda5..047677d543 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -48,7 +48,6 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { } } - internal fun MessagesEntryPoint.InitialTarget.toNavTarget() = when (this) { is MessagesEntryPoint.InitialTarget.Messages -> MessagesFlowNode.NavTarget.Messages(focusedEventId) MessagesEntryPoint.InitialTarget.PinnedMessages -> MessagesFlowNode.NavTarget.PinnedMessagesList diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 9e32655927..78832fa101 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -58,7 +58,6 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode -import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.overlay.Overlay @@ -114,7 +113,6 @@ class MessagesFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins ) { - sealed interface NavTarget : Parcelable { @Parcelize data object Empty : NavTarget diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 96cd9edfa9..4499ea70cf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -199,8 +199,6 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onJoinCallClick(room.roomId) } } - - @Composable override fun View(modifier: Modifier) { val context = LocalContext.current diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 057bd03477..f84e90edb4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -66,10 +66,9 @@ class DefaultActionListPresenter @AssistedInject constructor( private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, private val room: MatrixRoom, ) : ActionListPresenter { - @AssistedFactory @ContributesBinding(RoomScope::class) - interface Factory: ActionListPresenter.Factory { + interface Factory : ActionListPresenter.Factory { override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 0805a74655..12dcf15353 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -29,7 +29,7 @@ sealed class TimelineItemAction( @DrawableRes val icon: Int, val destructive: Boolean = false ) { - data object ViewInTimeline: TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on) + data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on) data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward) data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy) data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt index 0b05829f55..884429358c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionPostProcessor.kt @@ -24,5 +24,4 @@ fun interface TimelineItemActionPostProcessor { return actions } } - } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt index b574f1b6de..2ae9b35e0a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt @@ -34,11 +34,9 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode -import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index c6e02fdc18..a2b0238839 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.forward import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -28,16 +27,12 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.sql.Time class ForwardMessagesPresenter @AssistedInject constructor( @Assisted eventId: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultIsPinnedMessagesFeatureEnabled.kt similarity index 100% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultIsPinnedMessagesFeatureEnabled.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt index d3f6254c3e..6791080fa8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -58,7 +58,8 @@ class PinnedEventsTimelineProvider @Inject constructor( networkMonitor.connectivity ) { // do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed - isEnabled, _ -> isEnabled + isEnabled, _ -> + isEnabled } .onEach { isFeatureEnabled -> if (isFeatureEnabled) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 6d902ec332..072fa1c6d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -30,7 +30,6 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories -import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.di.RoomScope @@ -48,7 +47,6 @@ class PinnedMessagesListNode @AssistedInject constructor( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { - interface Callback : Plugin { fun onEventClick(event: TimelineItem.Event) fun onUserDataClick(userId: UserId) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 471f8d58ab..6c9ad5e4c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -69,7 +69,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private val snackbarDispatcher: SnackbarDispatcher, actionListPresenterFactory: ActionListPresenter.Factory, ) : Presenter { - @AssistedFactory interface Factory { fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt index 78279cf3d9..ae085fc327 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt @@ -20,7 +20,6 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor object PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProcessor { - override fun process(actions: List): List { return buildList { add(TimelineItemAction.ViewInTimeline) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 92e5c5e152..ae1f542258 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -46,7 +46,6 @@ import io.element.android.libraries.matrix.api.room.MessageEventType 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.timeline.ReceiptType -import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendMessageAsState diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index d2a66a2e1c..74f8fda0b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 71df68f34d..82ce258abd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 2fc33892ce..c6baa6d69a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -59,15 +59,16 @@ fun TimelineItemGroupedEventsRow( onReadReceiptClick: (TimelineItem.Event) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, - eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> - TimelineItemEventContentView( - content = event.content, - onLinkClick = onLinkClick, - eventSink = eventSink, - modifier = contentModifier, - onContentLayoutChange = onContentLayoutChange - ) - }, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = + { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { val isExpanded = rememberSaveable(key = timelineItem.identifier().value) { mutableStateOf(false) } @@ -120,15 +121,16 @@ private fun TimelineItemGroupedEventsRowContent( onReadReceiptClick: (TimelineItem.Event) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, - eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> - TimelineItemEventContentView( - content = event.content, - onLinkClick = onLinkClick, - eventSink = eventSink, - modifier = contentModifier, - onContentLayoutChange = onContentLayoutChange - ) - }, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = + { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { Column(modifier = modifier.animateContentSize()) { GroupHeaderView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 8f659c594f..84e7b35225 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -62,15 +62,16 @@ internal fun TimelineItemRow( onJoinCallClick: () -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, - eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { event, contentModifier, onContentLayoutChange -> - TimelineItemEventContentView( - content = event.content, - onLinkClick = onLinkClick, - eventSink = eventSink, - modifier = contentModifier, - onContentLayoutChange = onContentLayoutChange - ) - }, + eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = + { event, contentModifier, onContentLayoutChange -> + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + modifier = contentModifier, + onContentLayoutChange = onContentLayoutChange + ) + }, ) { val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) { val focusedEventOffset = if ((timelineItem as? TimelineItem.Event)?.showSenderInformation == true) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index 1a3d1bf566..ce882099ad 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -29,7 +29,6 @@ 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.toPersistentList -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index a75814d48e..49b807c568 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -22,12 +22,9 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter -import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory @@ -63,7 +60,6 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -104,7 +100,6 @@ import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.model.MessageComposerMode diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 06e311e79b..33a1133c67 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -33,8 +33,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE @@ -978,7 +976,7 @@ private fun createActionListPresenter( return DefaultActionListPresenter( postProcessor = TimelineItemActionPostProcessor.Default, appPreferencesStore = preferencesStore, - isPinnedMessagesFeatureEnabled = {isPinFeatureEnabled}, + isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled }, room = room ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt index 9e34c19567..0fab60f520 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor class FakeActionListPresenter : ActionListPresenter { - object Factory : ActionListPresenter.Factory { override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { return FakeActionListPresenter() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index a1eadc9342..8b389633c6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -36,12 +36,8 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt index 5d5f27a97a..c37287055b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt @@ -19,8 +19,7 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -class FakePinnedMessagesListNavigator: PinnedMessagesListNavigator { - +class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator { var onViewInTimelineClickLambda: ((EventId) -> Unit)? = null override fun onViewInTimelineClick(eventId: EventId) { onViewInTimelineClickLambda?.invoke(eventId) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index d3bf7efa24..64c3bcc3f0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class PinnedMessagesListPresenterTest { - @Test fun `present - initial state feature disabled`() = runTest { val room = FakeMatrixRoom( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index 2a76196376..6ec7a5d722 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -34,7 +34,6 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 4c726ab194..d1e4797a62 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -152,6 +152,4 @@ class RoomDetailsNode @AssistedInject constructor( onPinnedMessagesClick = ::openPinnedMessages ) } - - } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index a5a2f7f18a..6f663f10b2 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -127,7 +127,6 @@ class RoomDetailsViewTest { } } - @Config(qualifiers = "h1024dp") @Test fun `click on pinned messages invokes expected callback`() { diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt index 4eb13be279..8b6bd801e7 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt @@ -32,7 +32,6 @@ class DerivedStateFlow( private val getValue: () -> T, private val flow: Flow ) : StateFlow { - override val replayCache: List get() = listOf(value) From a634350a1b281b0735749b5043f07fc911837f10 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 15:04:20 +0200 Subject: [PATCH 19/22] Pinned messages : fix test after merging develop --- .../impl/pinned/list/PinnedMessagesListPresenterTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 64c3bcc3f0..ac53f191d3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -25,9 +25,11 @@ import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.timeline.FakeTimeline @@ -117,7 +119,7 @@ class PinnedMessagesListPresenterTest { timelineItems = flowOf( listOf( MatrixTimelineItem.Event( - uniqueId = "FAKE_UNIQUE_ID", + uniqueId = A_UNIQUE_ID, event = anEventTimelineItem( eventId = AN_EVENT_ID, content = messageContent, From ec4aab0c12f2022d7a2070bf86fded38620bd7c3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Sep 2024 17:43:34 +0200 Subject: [PATCH 20/22] Pinned messages list : add more test for better coverage. --- .../list/PinnedMessagesListPresenterTest.kt | 179 ++++++++++++++++-- 1 file changed, 164 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index ac53f191d3..44a4563720 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -18,16 +18,20 @@ package io.element.android.features.messages.impl.pinned.list import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -35,6 +39,9 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -114,20 +121,7 @@ class PinnedMessagesListPresenterTest { @Test fun `present - filled state`() = runTest { - val messageContent = aMessageContent("A message") - val pinnedEventsTimeline = FakeTimeline( - timelineItems = flowOf( - listOf( - MatrixTimelineItem.Event( - uniqueId = A_UNIQUE_ID, - event = anEventTimelineItem( - eventId = AN_EVENT_ID, - content = messageContent, - ), - ) - ) - ) - ) + val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeMatrixRoom( pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, @@ -149,6 +143,161 @@ class PinnedMessagesListPresenterTest { } } + @Test + fun `present - redact event`() = runTest { + val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) } + val pinnedEventsTimeline = createPinnedMessagesTimeline().apply { + this.redactEventLambda = redactEventLambda + } + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + val eventItem = filledState.timelineItems.first() as TimelineItem.Event + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem)) + cancelAndIgnoreRemainingEvents() + assert(redactEventLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID), value(null), value(null)) + } + } + + @Test + fun `present - unpin event`() = runTest { + val unpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) } + val pinnedEventsTimeline = createPinnedMessagesTimeline().apply { + this.unpinEventLambda = unpinEventLambda + } + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + val eventItem = filledState.timelineItems.first() as TimelineItem.Event + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) + cancelAndIgnoreRemainingEvents() + assert(unpinEventLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID)) + } + } + + @Test + fun `present - navigate to event`() = runTest { + val onViewInTimelineClickLambda = lambdaRecorder { _: EventId -> } + val navigator = FakePinnedMessagesListNavigator().apply { + this.onViewInTimelineClickLambda = onViewInTimelineClickLambda + } + val pinnedEventsTimeline = createPinnedMessagesTimeline() + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + val eventItem = filledState.timelineItems.first() as TimelineItem.Event + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem)) + cancelAndIgnoreRemainingEvents() + assert(onViewInTimelineClickLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID)) + } + } + + @Test + fun `present - show view source action`() = runTest { + val onShowEventDebugInfoClickLambda = lambdaRecorder { _: EventId?, _: TimelineItemDebugInfo -> } + val navigator = FakePinnedMessagesListNavigator().apply { + this.onShowEventDebugInfoClickLambda = onShowEventDebugInfoClickLambda + } + val pinnedEventsTimeline = createPinnedMessagesTimeline() + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + val eventItem = filledState.timelineItems.first() as TimelineItem.Event + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem)) + cancelAndIgnoreRemainingEvents() + assert(onShowEventDebugInfoClickLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID), value(eventItem.debugInfo)) + } + } + + @Test + fun `present - forward event`() = runTest { + val onForwardEventClickLambda = lambdaRecorder { _: EventId -> } + val navigator = FakePinnedMessagesListNavigator().apply { + this.onForwardEventClickLambda = onForwardEventClickLambda + } + val pinnedEventsTimeline = createPinnedMessagesTimeline() + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) + presenter.test { + skipItems(3) + val filledState = awaitItem() as PinnedMessagesListState.Filled + val eventItem = filledState.timelineItems.first() as TimelineItem.Event + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem)) + cancelAndIgnoreRemainingEvents() + assert(onForwardEventClickLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID)) + } + } + + private fun createPinnedMessagesTimeline(): FakeTimeline { + val messageContent = aMessageContent("A message") + return FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem( + eventId = AN_EVENT_ID, + content = messageContent, + ), + ) + ) + ) + ) + } + private fun TestScope.createPinnedMessagesListPresenter( navigator: PinnedMessagesListNavigator = FakePinnedMessagesListNavigator(), room: MatrixRoom = FakeMatrixRoom(), From a37a6d249f2b7e6dde48236b3c4015eb5539ae58 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 5 Sep 2024 17:36:16 +0200 Subject: [PATCH 21/22] Pinned messages list : improve and fix code after PR review. --- .../android/appnav/LoggedInFlowNode.kt | 7 +-- .../messages/api/MessagesEntryPoint.kt | 2 +- .../messages/impl/MessagesFlowNode.kt | 2 +- .../list/PinnedMessagesListPresenter.kt | 2 +- .../list/PinnedMessagesListStateProvider.kt | 49 ++++++++++++++++++- ...MessagesListTimelineActionPostProcessor.kt | 17 ++----- .../list/PinnedMessagesListPresenterTest.kt | 22 +++++++-- .../libraries/matrix/api/timeline/Timeline.kt | 2 +- .../matrix/impl/room/RustMatrixRoom.kt | 2 +- .../matrix/impl/timeline/RustTimeline.kt | 2 +- .../RoomBeginningPostProcessor.kt | 2 +- 11 files changed, 80 insertions(+), 29 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index d218d49dee..1d1fa2bea6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -36,7 +36,6 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace -import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom @@ -86,6 +85,7 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber import java.util.Optional +import java.util.UUID @ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -204,7 +204,8 @@ class LoggedInFlowNode @AssistedInject constructor( val serverNames: List = emptyList(), val trigger: JoinedRoom.Trigger? = null, val roomDescription: RoomDescription? = null, - val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages() + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(), + val targetId: UUID = UUID.randomUUID(), ) : NavTarget @Parcelize @@ -311,7 +312,7 @@ class LoggedInFlowNode @AssistedInject constructor( if (pushToBackstack) { backstack.push(target) } else { - backstack.singleTop(target) + backstack.replace(target) } } is PermalinkData.FallbackLink, diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 1bd1bfe756..219faae6b3 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -46,7 +46,7 @@ interface MessagesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onRoomDetailsClick() fun onUserDataClick(userId: UserId) - fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean = true) + fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onForwardedToSingleRoom(roomId: RoomId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 78832fa101..1f1b3f9b5d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -194,7 +194,7 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onPermalinkClick(data: PermalinkData) { - callbacks.forEach { it.onPermalinkClick(data) } + callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = true) } } override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 6c9ad5e4c9..c2e1a3baca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -74,7 +74,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter } - private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor) + private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor()) @Composable override fun present(): PinnedMessagesListState { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt index c98d3d0b75..eff0ebf961 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt @@ -21,10 +21,17 @@ import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo -import io.element.android.features.messages.impl.timeline.aTimelineItemList +import io.element.android.features.messages.impl.timeline.aTimelineItemDaySeparator +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList open class PinnedMessagesListStateProvider : PreviewParameterProvider { @@ -34,7 +41,45 @@ open class PinnedMessagesListStateProvider : PreviewParameterProvider): List { return buildList { add(TimelineItemAction.ViewInTimeline) - addAll(actions.filter(::predicate)) - } - } - - private fun predicate(action: TimelineItemAction): Boolean { - return when (action) { - is TimelineItemAction.Unpin, - is TimelineItemAction.Redact, - is TimelineItemAction.Forward, - is TimelineItemAction.ViewSource -> true - else -> false + actions.firstOrNull { it is TimelineItemAction.Unpin }?.let(::add) + actions.firstOrNull { it is TimelineItemAction.Forward }?.let(::add) + actions.firstOrNull { it is TimelineItemAction.ViewSource }?.let(::add) + actions.firstOrNull { it is TimelineItemAction.Redact }?.let(::add) } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 44a4563720..6f927123d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -172,10 +173,10 @@ class PinnedMessagesListPresenterTest { @Test fun `present - unpin event`() = runTest { - val unpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) } - val pinnedEventsTimeline = createPinnedMessagesTimeline().apply { - this.unpinEventLambda = unpinEventLambda - } + val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) } + val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure(A_THROWABLE) } + val pinnedEventsTimeline = createPinnedMessagesTimeline() + val room = FakeMatrixRoom( pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, @@ -189,9 +190,20 @@ class PinnedMessagesListPresenterTest { skipItems(3) val filledState = awaitItem() as PinnedMessagesListState.Filled val eventItem = filledState.timelineItems.first() as TimelineItem.Event + + pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) + + pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda + filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) + cancelAndIgnoreRemainingEvents() - assert(unpinEventLambda) + + assert(successUnpinEventLambda) + .isCalledOnce() + .with(value(AN_EVENT_ID)) + + assert(failureUnpinEventLambda) .isCalledOnce() .with(value(AN_EVENT_ID)) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 2eea2a6c2f..b4bc54d825 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -50,7 +50,7 @@ interface Timeline : AutoCloseable { enum class Mode { LIVE, FOCUSED_ON_EVENT, - FOCUSED_ON_PINNED_EVENTS + PINNED_EVENTS } val membershipChangeEventReceived: Flow diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 1430ad8025..abd1442c11 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -199,7 +199,7 @@ class RustMatrixRoom( internalIdPrefix = "pinned_events", maxEventsToLoad = 100u, ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_PINNED_EVENTS) + createTimeline(inner, mode = Timeline.Mode.PINNED_EVENTS) } }.onFailure { if (it is CancellationException) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index c2320d6a25..58a66c4db4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -137,7 +137,7 @@ class RustTimeline( private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val backPaginationStatus = MutableStateFlow( - Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.FOCUSED_ON_PINNED_EVENTS) + Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS) ) private val forwardPaginationStatus = MutableStateFlow( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index 2e90da88b5..fa53de6d2a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -37,7 +37,7 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { hasMoreToLoadBackwards: Boolean ): List { return when { - mode == Timeline.Mode.FOCUSED_ON_PINNED_EVENTS -> items + mode == Timeline.Mode.PINNED_EVENTS -> items hasMoreToLoadBackwards -> items isDm -> processForDM(items) else -> processForRoom(items) From 53a7bf43f24bbe24c25e17db6336c3264fd91965 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 5 Sep 2024 15:52:50 +0000 Subject: [PATCH 22/22] Update screenshots --- ...sages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png | 4 ++-- ...ges.impl.pinned.list_PinnedMessagesListView_Night_3_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png index ba8fae21e9..517e3104a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40c712c0518e86ac4fc3980fa9c31d50dc0371f08af4cf27d8d70202b42e3d9a -size 45758 +oid sha256:6fee1e714cc759547034e8e688e69ee525336e18febbbc118b7e1fdabe159a60 +size 42698 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png index f03d98bdd1..3db0b8ae49 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5d44ab86f523c740caef1e3a409e010dd100429f85ff806952b0a857cd3615 -size 46362 +oid sha256:7a16fcbc220880ccd0280e734bcdb6184b5a34a7505708f8f7795b6431cf6f41 +size 41511